diff options
author | Julien Gouesse <[email protected]> | 2023-03-07 00:32:36 +0100 |
---|---|---|
committer | Julien Gouesse <[email protected]> | 2023-03-07 00:32:36 +0100 |
commit | 275f2eb1ce54e7a719717324c13e8685a1ac67bd (patch) | |
tree | e9c36e1c156964d422df39a92d95853ed2512ada | |
parent | d8da0b55b161873e7bee159d4d9e5f4dec510270 (diff) |
Improves the OFF importer, which can now support meshes with colors on vertices
3 files changed, 301 insertions, 43 deletions
diff --git a/ardor3d-examples/src/main/java/com/ardor3d/example/pipeline/SimpleOffExample.java b/ardor3d-examples/src/main/java/com/ardor3d/example/pipeline/SimpleOffExample.java index c8b3f19..be255dc 100644 --- a/ardor3d-examples/src/main/java/com/ardor3d/example/pipeline/SimpleOffExample.java +++ b/ardor3d-examples/src/main/java/com/ardor3d/example/pipeline/SimpleOffExample.java @@ -36,7 +36,7 @@ public class SimpleOffExample extends ExampleBase { // Load the OFF scene final long time = System.currentTimeMillis(); final OffImporter importer = new OffImporter(); - final OffGeometryStore storage = importer.load("off/cube.off"); + final OffGeometryStore storage = importer.load("off/vertcube.off"); System.out.println("Importing Took " + (System.currentTimeMillis() - time) + " ms"); final Node model = storage.getScene(); diff --git a/ardor3d-extras/src/main/java/com/ardor3d/extension/model/off/OffFaceInfo.java b/ardor3d-extras/src/main/java/com/ardor3d/extension/model/off/OffFaceInfo.java index d0cec71..60d24a7 100644 --- a/ardor3d-extras/src/main/java/com/ardor3d/extension/model/off/OffFaceInfo.java +++ b/ardor3d-extras/src/main/java/com/ardor3d/extension/model/off/OffFaceInfo.java @@ -19,6 +19,7 @@ public class OffFaceInfo { private List<Integer> _materialIndices; + // TODO use only vertices indices and colors in faces private List<Float> _textureCoordinates; public OffFaceInfo() { diff --git a/ardor3d-extras/src/main/java/com/ardor3d/extension/model/off/OffImporter.java b/ardor3d-extras/src/main/java/com/ardor3d/extension/model/off/OffImporter.java index b63a3b0..74c44e3 100644 --- a/ardor3d-extras/src/main/java/com/ardor3d/extension/model/off/OffImporter.java +++ b/ardor3d-extras/src/main/java/com/ardor3d/extension/model/off/OffImporter.java @@ -17,17 +17,26 @@ import java.io.InputStreamReader; import java.io.Reader; import java.io.StreamTokenizer; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; import java.util.logging.Level; import java.util.logging.Logger; -import java.util.stream.Stream; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import com.ardor3d.math.ColorRGBA; +import com.ardor3d.math.Vector2; +import com.ardor3d.math.Vector3; import com.ardor3d.util.geom.GeometryTool; import com.ardor3d.util.resource.ResourceLocator; import com.ardor3d.util.resource.ResourceLocatorTool; import com.ardor3d.util.resource.ResourceSource; /** - * OFF importer. See <a href="http://paulbourke.net/dataformats/off/">the format spec</a> + * OFF importer. See <a href="http://paulbourke.net/dataformats/off/">the format spec</a> and + * <a href="http://www.geomview.org/docs/html/OFF.html">Geomview's documentation</a> * * N.B: WORK IN PROGRESS, STILL A LOT OF WORK TO DO * @@ -37,6 +46,129 @@ import com.ardor3d.util.resource.ResourceSource; */ public class OffImporter { + private enum OffKeyword { + /** + * + */ + _OFF("off", 3, 0, 0, 0), + /** + * + */ + _COFF("coff", 3, 0, 4, 0), + /** + * + */ + _CNOFF("cnoff", 3, 3, 4, 0), + /** + * + */ + _NOFF("noff", 3, 3, 0, 0), + /** + * + */ + _STCOFF("coff", 3, 0, 4, 2), + /** + * + */ + _STCNOFF("cnoff", 3, 3, 4, 2), + /** + * + */ + _STNOFF("noff", 3, 3, 0, 2), + /** + * + */ + _4OFF("4off", 4, 0, 0, 0), + /** + * + */ + _C4OFF("c4off", 4, 0, 4, 0), + /** + * + */ + _CN4OFF("cn4off", 4, 4, 4, 0), + /** + * + */ + _N4OFF("n4off", 4, 4, 0, 0), + /** + * + */ + _STC4OFF("c4off", 4, 0, 4, 2), + /** + * + */ + _STCN4OFF("cn4off", 4, 4, 4, 2), + /** + * + */ + _STN4OFF("n4off", 4, 4, 0, 2); + + /** + * lowercase text of the keyword as found in the very beginning of the file + */ + private final String lowercaseKeywordText; + + /** + * vertex values per tuple, either 3 or 4 + */ + private final int vertexValuesPerTuple; + + /** + * normal values per tuple, either 0, 3 or 4 + */ + private final int normalValuesPerTuple; + + /** + * minimum color values per tuple, either 0 or 1 + */ + private final int minColorValuesPerTuple; + + /** + * maximum color values per tuple, either 0, 1 or 4, color map, RGB and RGBA colors are supported + */ + private final int maxColorValuesPerTuple; + + /** + * texture values per tuple, either 0 or 2 + */ + private final int textureValuesPerTuple; + + private OffKeyword(final String lowercaseKeywordText, final int vertexValuesPerTuple, + final int normalValuesPerTuple, final int maxColorValuesPerTuple, final int textureValuesPerTuple) { + this.lowercaseKeywordText = lowercaseKeywordText; + this.vertexValuesPerTuple = vertexValuesPerTuple; + this.normalValuesPerTuple = normalValuesPerTuple; + minColorValuesPerTuple = Math.min(1, maxColorValuesPerTuple); + this.maxColorValuesPerTuple = maxColorValuesPerTuple; + this.textureValuesPerTuple = textureValuesPerTuple; + } + + public String getLowercaseKeywordText() { + return lowercaseKeywordText; + } + + public int getVertexValuesPerTuple() { + return vertexValuesPerTuple; + } + + public int getNormalValuesPerTuple() { + return normalValuesPerTuple; + } + + public int getMinColorValuesPerTuple() { + return minColorValuesPerTuple; + } + + public int getMaxColorValuesPerTuple() { + return maxColorValuesPerTuple; + } + + public int getTextureValuesPerTuple() { + return textureValuesPerTuple; + } + } + // public static interface OffReader extends Closeable { // public double read() throws IOException; // } @@ -210,8 +342,8 @@ public class OffImporter { new BufferedReader(new InputStreamReader(resource.openStream(), StandardCharsets.US_ASCII)))) { try { final Integer numberOfVertices; - Integer numberOfFaces = null; - Integer numberOfEdges = null; + final Integer numberOfFaces; + final Integer numberOfEdges; // starts reading the file as ascii (binary file not supported) // skips the commented line(s) do { @@ -223,15 +355,17 @@ public class OffImporter { "Premature end of file, expected an optional off keyword followed by three integers vertex_count face_count edge_count"); } final String unhandledFirstParsedValue; - final int coordinateCountPerVertex; - // tries to read "off", "coff", "noff", "cnoff" or "4off" (optional) - if (Stream.of("off", "coff", "noff", "cnoff", "4off").anyMatch(parser.sval::equals)) { - OffImporter.LOGGER.log(Level.INFO, parser.sval + " keyword on line " + parser.lineno()); - unhandledFirstParsedValue = null; - } else { + final OffKeyword offKeywordInFile = Arrays.stream(OffKeyword.values()).filter( + (final OffKeyword offKeyword) -> offKeyword.getLowercaseKeywordText().equals(parser.sval)) + .findFirst().orElse(null); + // tries to read an (optional) off keyword + if (offKeywordInFile == null) { // no *off keyword OffImporter.LOGGER.log(Level.INFO, "No off keyword on line " + parser.lineno()); unhandledFirstParsedValue = parser.sval; + } else { + OffImporter.LOGGER.log(Level.INFO, parser.sval + " keyword on line " + parser.lineno()); + unhandledFirstParsedValue = null; } parser.nextToken(); if (parser.ttype == StreamTokenizer.TT_EOF) { @@ -285,6 +419,7 @@ public class OffImporter { parser.nextToken(); if (parser.ttype == StreamTokenizer.TT_WORD) { numberOfEdges = Integer.valueOf(parser.sval); + OffImporter.LOGGER.log(Level.INFO, "Number of edges: " + numberOfEdges); } else { throw new IOException( "Premature end of line, expected three integers vertex_count face_count edge_count"); @@ -300,51 +435,173 @@ public class OffImporter { if (parser.ttype == StreamTokenizer.TT_EOF) { throw new IOException("Premature end of file, no vertex has been declared"); } - // reads the vertex coordinates - // TODO read the color and normal coordinates on vertices + // when there is no off keyword in the file, it takes off keyword's behaviour + final OffKeyword offKeyword = Optional.ofNullable(offKeywordInFile).orElse(OffKeyword._OFF); + // computes the expected value counts per line of vertex definition + final int expectedValueCountPerVertexLineExcludingColorValues = offKeyword.getVertexValuesPerTuple() + + offKeyword.getNormalValuesPerTuple() + offKeyword.getTextureValuesPerTuple(); + final int minExpectedValueCountPerVertexLine = expectedValueCountPerVertexLineExcludingColorValues + + offKeyword.getMinColorValuesPerTuple(); + final int maxExpectedValueCountPerVertexLine = expectedValueCountPerVertexLineExcludingColorValues + + offKeyword.getMaxColorValuesPerTuple(); + // reads the vertices, normals, colors and/or texture coordinates for each vertex in that order for (int vertexIndex = 0; vertexIndex < numberOfVertices.intValue(); vertexIndex++) { + // skips comment lines, comments and empty lines do { parser.nextToken(); - } while (parser.ttype != StreamTokenizer.TT_WORD && parser.ttype != StreamTokenizer.TT_EOF - && parser.ttype != StreamTokenizer.TT_EOL); - // expects between 1 and 4 vertex coordinates per line - if (parser.ttype == StreamTokenizer.TT_EOF || parser.ttype == StreamTokenizer.TT_EOL) { - throw new IOException("Premature end of line, expected 1, 2, 3 or 4 vertex coordinates"); - } - final Double x, y, z, w; - x = Double.valueOf(parser.sval); - parser.nextToken(); + } while (parser.ttype != StreamTokenizer.TT_WORD && parser.ttype != StreamTokenizer.TT_EOF); if (parser.ttype == StreamTokenizer.TT_EOF) { - throw new IOException("Premature end of file, no face has been declared"); + throw new IOException("Premature end of file, expected " + numberOfVertices.intValue() + + " vertices, found " + vertexIndex); } - if (parser.ttype == StreamTokenizer.TT_WORD) { - y = Double.valueOf(parser.sval).doubleValue(); + // keeps the current word + parser.pushBack(); + // reads as much numbers as possible on the line + boolean goOn = true; + final List<Number> valueList = new ArrayList<>(); + do { parser.nextToken(); - if (parser.ttype == StreamTokenizer.TT_EOF) { - throw new IOException("Premature end of file, no face has been declared"); + switch (parser.ttype) { + case StreamTokenizer.TT_WORD: + if (parser.sval.contains(",") || parser.sval.contains(".")) { + valueList.add(Double.valueOf(parser.sval)); + } else { + valueList.add(Integer.valueOf(parser.sval)); + } + break; + case StreamTokenizer.TT_EOL: + goOn = false; + break; + default: + // the premature end of file is handled elsewhere, keeps the current state + parser.pushBack(); + break; } - if (parser.ttype == StreamTokenizer.TT_WORD) { - z = Double.valueOf(parser.sval).doubleValue(); - parser.nextToken(); - if (parser.ttype == StreamTokenizer.TT_EOF) { - throw new IOException("Premature end of file, no face has been declared"); - } - if (parser.ttype == StreamTokenizer.TT_WORD) { - w = Double.valueOf(parser.sval).doubleValue(); + } while (goOn); + if (minExpectedValueCountPerVertexLine <= valueList.size()) { + if (valueList.size() <= maxExpectedValueCountPerVertexLine) { + OffImporter.LOGGER.log(Level.INFO, "Coords: " + + valueList.stream().map(Number::toString).collect(Collectors.joining(" "))); + // TODO put the values into the data store (vertices, normals, colors and/or texture + // coordinates) + if (offKeyword.getVertexValuesPerTuple() == 3) { + store.getDataStore().getVertices().add(new Vector3(valueList.get(0).doubleValue(), + valueList.get(1).doubleValue(), valueList.get(2).doubleValue())); } else { - w = null; + // TODO 4D + } + int nextIndex = offKeyword.getVertexValuesPerTuple(); + switch (offKeyword.getNormalValuesPerTuple()) { + case 0: + // nothing to do + break; + case 3: + store.getDataStore().getNormals() + .add(new Vector3(valueList.get(nextIndex).doubleValue(), + valueList.get(nextIndex + 1).doubleValue(), + valueList.get(nextIndex + 2).doubleValue())); + break; + case 4: + // TODO 4D + break; + } + nextIndex += offKeyword.getNormalValuesPerTuple(); + final int colorValuesPerTuple = valueList.size() - offKeyword.getVertexValuesPerTuple() + - offKeyword.getNormalValuesPerTuple() - offKeyword.getTextureValuesPerTuple(); + switch (colorValuesPerTuple) { + case 0: + // nothing to do + break; + case 1: + // TODO store the color map somewhere + break; + case 3: + final ColorRGBA rgb = new ColorRGBA(valueList.get(nextIndex).floatValue(), + valueList.get(nextIndex + 1).floatValue(), + valueList.get(nextIndex + 2).floatValue(), 0.0f); + if (valueList.get(nextIndex) instanceof Integer) { + rgb.divideLocal(255.0f); + } + rgb.setAlpha(1.0f); + store.getDataStore().getColors().add(rgb); + break; + case 4: + final ColorRGBA rgba = new ColorRGBA(valueList.get(nextIndex).floatValue(), + valueList.get(nextIndex + 1).floatValue(), + valueList.get(nextIndex + 2).floatValue(), + valueList.get(nextIndex + 3).floatValue()); + if (valueList.get(nextIndex) instanceof Integer) { + rgba.divideLocal(255.0f); + } + rgba.setAlpha(1.0f); + store.getDataStore().getColors().add(rgba); + break; + } + nextIndex += colorValuesPerTuple; + if (offKeyword.getTextureValuesPerTuple() == 2) { + store.getDataStore().getTextureCoordinates() + .add(new Vector2(valueList.get(nextIndex).doubleValue(), + valueList.get(nextIndex + 1).doubleValue())); } } else { - z = null; - w = null; + throw new IOException("Too much values per (vertex) line, expected at most " + + maxExpectedValueCountPerVertexLine + " values, got " + valueList.size()); + } + } else { + throw new IOException("Premature end of (vertex) line, expected at least " + + minExpectedValueCountPerVertexLine + " values, got " + valueList.size()); + } + } + for (int faceIndex = 0; faceIndex < numberOfFaces.intValue(); faceIndex++) { + // skips comment lines, comments and empty lines + do { + parser.nextToken(); + } while (parser.ttype != StreamTokenizer.TT_WORD && parser.ttype != StreamTokenizer.TT_EOF); + if (parser.ttype == StreamTokenizer.TT_EOF) { + throw new IOException("Premature end of file, expected " + numberOfFaces.intValue() + + " faces, found " + faceIndex); + } + // keeps the current word + parser.pushBack(); + // reads as much numbers as possible on the line + boolean goOn = true; + final List<Number> valueList = new ArrayList<>(); + do { + parser.nextToken(); + switch (parser.ttype) { + case StreamTokenizer.TT_WORD: + if (parser.sval.contains(",") || parser.sval.contains(".")) { + valueList.add(Double.valueOf(parser.sval)); + } else { + valueList.add(Integer.valueOf(parser.sval)); + } + break; + case StreamTokenizer.TT_EOL: + goOn = false; + break; + default: + // the premature end of file is handled elsewhere, keeps the current state + parser.pushBack(); + break; } + } while (goOn); + if (valueList.isEmpty()) { + throw new IOException( + "Premature end of (face) line, expected at least one value, got zero"); } else { - y = null; - z = null; - w = null; + OffImporter.LOGGER.log(Level.INFO, "Face coords: " + + valueList.stream().map(Number::toString).collect(Collectors.joining(" "))); + // puts the indices into the geometry store + final int vertexIndexCount = valueList.get(0).intValue(); + final OffFaceInfo faceInfo = new OffFaceInfo(); + IntStream.rangeClosed(1, vertexIndexCount).mapToObj(valueList::get) + .mapToInt(Number::intValue).forEachOrdered(faceInfo::addVertexIndex); + final int colorComponentCount = valueList.size() - vertexIndexCount - 1; + if (0 < colorComponentCount) { + // TODO put the colors into the geometry store + } + store.addFace(faceInfo); } - // TODO support more cases but without ambiguities - OffImporter.LOGGER.log(Level.INFO, "x: " + x + " y: " + y + " z: " + z + " w: " + w); } } catch (final IOException ioe) { throw new Exception("IO Error on line " + parser.lineno(), ioe); |