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 }