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 }