View Javadoc

    1   /*
    2    * Licensed to the Apache Software Foundation (ASF) under one or more
    3    * contributor license agreements.  See the NOTICE file distributed with
    4    * this work for additional information regarding copyright ownership.
    5    * The ASF licenses this file to You under the Apache License, Version 2.0
    6    * (the "License"); you may not use this file except in compliance with
    7    * the License.  You may obtain a copy of the License at
    8    *
    9    *      http://www.apache.org/licenses/LICENSE-2.0
    10   *
    11   * Unless required by applicable law or agreed to in writing, software
    12   * distributed under the License is distributed on an "AS IS" BASIS,
    13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    14   * See the License for the specific language governing permissions and
    15   * limitations under the License.
    16   */
    17  package org.apache.commons.geometry.examples.io.threed.obj;
    18
    19  import java.io.File;
    20  import java.io.IOException;
    21  import java.io.InputStreamReader;
    22  import java.io.Reader;
    23  import java.net.URL;
    24  import java.nio.file.Files;
    25
    26  import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
    27  import org.apache.commons.geometry.euclidean.threed.Vector3D;
    28  import org.apache.commons.geometry.euclidean.threed.mesh.SimpleTriangleMesh;
    29  import org.apache.commons.geometry.euclidean.threed.mesh.TriangleMesh;
    30
    31  /** Class for reading {@link TriangleMesh} objects from OBJ files. Only vertex and face definitions
    32   * are read from the input; other OBJ keywords are ignored.
    33   *
    34   * <p>Instances of this class are <em>not</em> thread-safe.</p>
    35   * @see <a href="https://en.wikipedia.org/wiki/Wavefront_.obj_file">Wavefront .obj file</a>
    36   */
    37  public final class OBJReader {
    38
    39      /** Character buffer size. */
    40      private static final int BUFFER_SIZE = 2048;
    41
    42      /** Builder object used to construct the mesh. */
    43      private SimpleTriangleMesh.Builder meshBuilder;
    44
    45      /** Read a {@link TriangleMesh} from the given OBJ file. The file is read using the UTF-8 charset.
    46       * @param file file to read from
    47       * @param precision precision context to use in the created mesh
    48       * @return a new mesh object
    49       * @throws IOException if an IO operation fails
    50       * @throws IllegalArgumentException if invalid OBj syntax is encountered in the input
    51       */
    52      public TriangleMesh readTriangleMesh(final File file, final DoublePrecisionContext precision) throws IOException {
    53          try (Reader reader = Files.newBufferedReader(file.toPath(), OBJConstants.DEFAULT_CHARSET)) {
    54              return readTriangleMesh(reader, precision);
    55          }
    56      }
    57
    58      /** Read a {@link TriangleMesh} from the given url representing an OBJ file. The input is read using the
    59       * UTF-8 charset.
    60       * @param url url to read from
    61       * @param precision precision context to use in the created mesh
    62       * @return a new mesh object
    63       * @throws IOException if an IO operation fails
    64       * @throws IllegalArgumentException if invalid OBj syntax is encountered in the input
    65       */
    66      public TriangleMesh readTriangleMesh(final URL url, final DoublePrecisionContext precision) throws IOException {
    67          try (InputStreamReader reader = new InputStreamReader(url.openStream(), OBJConstants.DEFAULT_CHARSET)) {
    68              return readTriangleMesh(reader, precision);
    69          }
    70      }
    71
    72      /** Read a {@link TriangleMesh} from the given reader. The reader is not closed.
    73       * @param reader the reader to read input from
    74       * @param precision precision context to use in the created mesh
    75       * @return a new mesh object
    76       * @throws IOException if an IO operation fails
    77       * @throws IllegalArgumentException if invalid OBj syntax is encountered in the input
    78       */
    79      public TriangleMesh readTriangleMesh(final Reader reader, final DoublePrecisionContext precision)
    80              throws IOException {
    81          meshBuilder = SimpleTriangleMesh.builder(precision);
    82
    83          parse(reader);
    84
    85          final TriangleMesh mesh = meshBuilder.build();
    86          meshBuilder = null;
    87
    88          return mesh;
    89      }
    90
    91      /** Parse the input from the reader.
    92       * @param reader reader to read from
    93       * @throws IOException if an IO error occurs
    94       * @throws IllegalArgumentException if invalid OBj syntax is encountered
    95       */
    96      private void parse(final Reader reader) throws IOException {
    97          final char[] buffer = new char[BUFFER_SIZE];
    98          final StringBuilder sb = new StringBuilder();
    99
    100         char ch;
    101         int read;
    102         while ((read = reader.read(buffer, 0, buffer.length)) > 0) {
    103
    104             for (int i = 0; i < read; ++i) {
    105                 ch = buffer[i];
    106
    107                 if (ch == '\r' || ch == '\n') {
    108                     if (sb.length() > 0) {
    109                         parseLine(sb.toString());
    110
    111                         sb.delete(0, sb.length());
    112                     }
    113                 } else {
    114                     sb.append(ch);
    115                 }
    116             }
    117         }
    118
    119         if (sb.length() > 0) {
    120             parseLine(sb.toString());
    121         }
    122     }
    123
    124     /** Parse a line read from the input.
    125      * @param line line to parse
    126      * @throws IllegalArgumentException if invalid OBj syntax is encountered
    127      */
    128     private void parseLine(final String line) {
    129         // advance past any preceding whitespace
    130         int startIdx = 0;
    131         while (startIdx < line.length() && Character.isWhitespace(line.charAt(startIdx))) {
    132             ++startIdx;
    133         }
    134
    135         if (startIdx >= line.length() || line.charAt(startIdx) == OBJConstants.COMMENT_START_CHAR) {
    136             return; // skip
    137         }
    138
    139         final int idx = nextWhitespace(line, startIdx);
    140         if (idx > -1) {
    141             final String keyword = line.substring(startIdx, idx);
    142             final String remainder = line.substring(idx + 1).trim();
    143
    144             // we're only interested in vertex and face lines; ignore everything else
    145             if (OBJConstants.VERTEX_KEYWORD.equals(keyword)) {
    146                 parseVertexLine(remainder);
    147             } else if (OBJConstants.FACE_KEYWORD.equals(keyword)) {
    148                 parseFaceLine(remainder);
    149             }
    150         }
    151     }
    152
    153     /** Parse a vertex definition line.
    154      * @param line line content, excluding the initial vertex keyword
    155      * @throws IllegalArgumentException if invalid OBj syntax is encountered
    156      */
    157     private void parseVertexLine(final String line) {
    158         final String[] parts = splitOnWhitespace(line);
    159         if (parts.length < 3) {
    160             throw new IllegalArgumentException(
    161                     "Invalid vertex definition: at least 3 fields required but found only " + parts.length);
    162         }
    163
    164         final double x = Double.parseDouble(parts[0]);
    165         final double y = Double.parseDouble(parts[1]);
    166         final double z = Double.parseDouble(parts[2]);
    167
    168         addVertex(Vector3D.of(x, y, z));
    169     }
    170
    171     /** Add a vertex to the constructed mesh.
    172      * @param vertex vertex to add
    173      */
    174     private void addVertex(final Vector3D vertex) {
    175         meshBuilder.addVertex(vertex);
    176     }
    177
    178     /** Parse a face definition line.
    179      * @param line line content, excluding the initial face keyword
    180      * @throws IllegalArgumentException if invalid OBj syntax is encountered
    181      */
    182     private void parseFaceLine(final String line) {
    183         final String[] parts = splitOnWhitespace(line);
    184         if (parts.length < 3) {
    185             throw new IllegalArgumentException(
    186                     "Invalid face definition: at least 3 fields required but found only " + parts.length);
    187         }
    188
    189         // use a simple triangle fan if more than 3 vertices are given
    190         final int startIdx = parseFaceVertexIndex(parts[0]);
    191         int prevIdx = parseFaceVertexIndex(parts[1]);
    192         int curIdx;
    193         for (int i = 2; i < parts.length; ++i) {
    194             curIdx = parseFaceVertexIndex(parts[i]);
    195             addFace(startIdx, prevIdx, curIdx);
    196
    197             prevIdx = curIdx;
    198         }
    199     }
    200
    201     /** Parse the vertex index from a face vertex definition of the form {@code v/vt/vn},
    202      * where {@code v} is the vertex index, {@code vt} is the vertex texture coordinate, and
    203      * {@code vn} is the vertex normal index. The texture coordinate and normal are optional and
    204      * are ignored by this class.
    205      * @param str string to parse
    206      * @return the face vertex index
    207      */
    208     private int parseFaceVertexIndex(final String str) {
    209         final int sepIdx = str.indexOf(OBJConstants.FACE_VALUE_SEP_CHAR);
    210         final String vertexIdxStr = sepIdx > -1 ?
    211                 str.substring(0, sepIdx) :
    212                 str;
    213
    214         return Integer.parseInt(vertexIdxStr);
    215     }
    216
    217     /** Add a face to the constructed mesh.
    218      * @param index1 first vertex index, in OBJ format
    219      * @param index2 second vertex index, in OBJ format
    220      * @param index3 third vertex index, in OBJ format
    221      */
    222     private void addFace(final int index1, final int index2, final int index3) {
    223         meshBuilder.addFace(
    224                 adjustVertexIndex(index1),
    225                 adjustVertexIndex(index2),
    226                 adjustVertexIndex(index3));
    227     }
    228
    229     /** Adjust a vertex index from the OBJ format to array index format. OBJ vertex indices
    230      * are 1-based and are allowed to be negative to refer to indices added most recently. For
    231      * example, index {@code 1} refers to the first added vertex and {@code -1} refers to the
    232      * most recently added vertex.
    233      * @param index index to adjust
    234      * @return the adjusted 0-based index
    235      */
    236     private int adjustVertexIndex(final int index) {
    237         if (index < 0) {
    238             // relative index from end
    239             return meshBuilder.getVertexCount() + index;
    240         }
    241
    242         // convert from 1-based to 0-based
    243         return index - 1;
    244     }
    245
    246     /** Find the index of the next whitespace character in the string.
    247      * @param str string to search
    248      * @param startIdx index to begin the search
    249      * @return the index of the next whitespace character or null if not found
    250      */
    251     private int nextWhitespace(final String str, final int startIdx) {
    252         final int len = str.length();
    253         for (int i = startIdx; i < len; ++i) {
    254             if (Character.isWhitespace(str.charAt(i))) {
    255                 return i;
    256             }
    257         }
    258
    259         return -1;
    260     }
    261
    262     /** Split the given string on whitespace characters.
    263      * @param str string to split
    264      * @return the split string sections
    265      */
    266     private String[] splitOnWhitespace(final String str) {
    267         return str.split("\\s+");
    268     }
    269 }