Skip to content

Commit

Permalink
OBJ Mesh Files
Browse files Browse the repository at this point in the history
- Reading texture mapped files now works
PointCloudIO
- Modified how it reads from files
  • Loading branch information
lessthanoptimal committed Jun 23, 2024
1 parent 3249a5a commit ee21e50
Show file tree
Hide file tree
Showing 11 changed files with 611 additions and 85 deletions.
17 changes: 2 additions & 15 deletions applications/src/main/java/boofcv/app/MeshViewerApp.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,11 @@
import boofcv.struct.image.ImageType;
import boofcv.struct.image.InterleavedU8;
import boofcv.struct.mesh.VertexMesh;
import org.apache.commons.io.FilenameUtils;

import javax.swing.*;
import java.awt.*;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Locale;

/**
* Very simple app for opening and viewing a 3D mesh
Expand All @@ -48,18 +45,8 @@ public MeshViewerApp() {
private static void loadFile( File file ) {
// Load the mesh
var mesh = new VertexMesh();
String extension = FilenameUtils.getExtension(file.getName()).toLowerCase(Locale.ENGLISH);
var type = switch (extension) {
case "ply" -> PointCloudIO.Format.PLY;
case "stl" -> PointCloudIO.Format.STL;
case "obj" -> PointCloudIO.Format.OBJ;
default -> {
throw new RuntimeException("Unknown file type");
}
};

try (var input = new FileInputStream(file)) {
PointCloudIO.load(type, input, mesh);
try {
PointCloudIO.load(file, mesh);
} catch (IOException e) {
e.printStackTrace(System.err);
System.exit(1);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2021, Peter Abeles. All Rights Reserved.
* Copyright (c) 2024, Peter Abeles. All Rights Reserved.
*
* This file is part of BoofCV (http://boofcv.org).
*
Expand Down Expand Up @@ -38,6 +38,9 @@ public interface PointCloudReader {
*/
int size();

/** True if each point has a color */
boolean colors();

/**
* Copies the point
*/
Expand All @@ -58,6 +61,8 @@ static PointCloudReader wrap3FRGB( float[] cloud, float[] rgb, int offset, int l
@Override
public int size() {return length;}

@Override public boolean colors() {return true;}

@Override
public void get( int index, Point3D_F32 point ) {
int i = offset + index*3;
Expand Down Expand Up @@ -85,6 +90,8 @@ static PointCloudReader wrapF32( List<Point3D_F32> cloud ) {
return new PointCloudReader() {
@Override public int size() {return cloud.size();}

@Override public boolean colors() {return false;}

@Override public void get( int index, Point3D_F32 point ) {point.setTo(cloud.get(index));}

@Override public void get( int index, Point3D_F64 point ) {convert(cloud.get(index), point);}
Expand All @@ -97,6 +104,8 @@ static PointCloudReader wrapF64( List<Point3D_F64> cloud ) {
return new PointCloudReader() {
@Override public int size() {return cloud.size();}

@Override public boolean colors() {return false;}

@Override public void get( int index, Point3D_F32 point ) {convert(cloud.get(index), point);}

@Override public void get( int index, Point3D_F64 point ) {point.setTo(cloud.get(index));}
Expand All @@ -109,6 +118,8 @@ static PointCloudReader wrapF32RGB( List<Point3dRgbI_F32> cloud ) {
return new PointCloudReader() {
@Override public int size() {return cloud.size();}

@Override public boolean colors() {return true;}

@Override public void get( int index, Point3D_F32 point ) {point.setTo(cloud.get(index));}

@Override public void get( int index, Point3D_F64 point ) {convert(cloud.get(index), point);}
Expand All @@ -121,6 +132,8 @@ static PointCloudReader wrapF64RGB( List<Point3dRgbI_F64> cloud ) {
return new PointCloudReader() {
@Override public int size() {return cloud.size();}

@Override public boolean colors() {return true;}

@Override public void get( int index, Point3D_F32 point ) {convert(cloud.get(index), point);}

@Override public void get( int index, Point3D_F64 point ) {point.setTo(cloud.get(index));}
Expand All @@ -133,6 +146,8 @@ static PointCloudReader wrapF32( List<Point3D_F32> cloud, int[] rgb ) {
return new PointCloudReader() {
@Override public int size() {return cloud.size();}

@Override public boolean colors() {return true;}

@Override public void get( int index, Point3D_F32 point ) {point.setTo(cloud.get(index));}

@Override public void get( int index, Point3D_F64 point ) {convert(cloud.get(index), point);}
Expand All @@ -145,6 +160,8 @@ static PointCloudReader wrapF64( List<Point3D_F64> cloud, int[] rgb ) {
return new PointCloudReader() {
@Override public int size() {return cloud.size();}

@Override public boolean colors() {return true;}

@Override public void get( int index, Point3D_F32 point ) {convert(cloud.get(index), point);}

@Override public void get( int index, Point3D_F64 point ) {point.setTo(cloud.get(index));}
Expand All @@ -159,6 +176,8 @@ static PointCloudReader wrap( Generic op, int size ) {

@Override public int size() {return size;}

@Override public boolean colors() {return true;}

@Override public void get( int index, Point3D_F32 point ) {
op.get(index, p);
point.x = (float)p.x;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,25 @@ static PointCloudWriter wrapF64( DogArray<Point3D_F64> cloud ) {
};
}

static PointCloudWriter wrapF64( DogArray<Point3D_F64> cloud, DogArray_I32 colors ) {
return new PointCloudWriter() {
@Override public void initialize( int size, boolean hasColor ) {
cloud.reserve(size);
cloud.reset();
colors.reserve(size);
colors.reset();
}

@Override public void startPoint() {}

@Override public void stopPoint() {}

@Override public void color( int rgb ) {colors.add(rgb);}

@Override public void location( double x, double y, double z ) {cloud.grow().setTo(x, y, z);}
};
}

static PointCloudWriter wrapF32RGB( DogArray<Point3dRgbI_F32> cloud ) {
return new PointCloudWriter() {
@Override public void initialize( int size, boolean hasColor ) {
Expand Down
24 changes: 24 additions & 0 deletions main/boofcv-io/src/main/java/boofcv/io/points/PointCloudIO.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.util.Locale;
import java.util.Map;

/**
* Code for reading different point cloud formats
Expand Down Expand Up @@ -95,6 +96,8 @@ public static void save3D( Format format,

@Override public int size() {return size;}

@Override public boolean colors() {return true;}

@Override public void get( int index, Point3D_F64 point ) {accessPoint.getPoint(index, point);}

@Override public int getRGB( int index ) {return accessColor.getRGB(index);}
Expand Down Expand Up @@ -148,6 +151,8 @@ public static void load( Format format, InputStream input, PointCloudWriter outp
/**
* Loads the mesh from a file. File type is determined by the file's extension.
*
* <p>For OBJ files, if there are multiple shapes defined then only the first one is returned.</p>
*
* @param file Which file it should load
* @param mesh (Output) storage for the mesh
*/
Expand All @@ -160,11 +165,30 @@ public static void load( File file, VertexMesh mesh ) throws IOException {
default -> throw new RuntimeException("Unknown file type: " + extension);
};


// OBJ files are special. They need to read in multiple files to get the texture map image and
// there can be multiple shapes defined. This will handle all those situations
if (type == PointCloudIO.Format.OBJ) {
var reader = new ObjLoadFromFiles();
reader.load(file, mesh);
return;
}

try (var input = new FileInputStream(file)) {
PointCloudIO.load(type, input, mesh);
}
}

/**
* Loads a set of {@link VertexMesh} from an OBJ file. This can ready any type of OBJ file as it doesn't make
* assumptions about what is contained inside of it
*/
public static Map<String, VertexMesh> loadObj( File file ) {
var reader = new ObjLoadFromFiles();
reader.load(file, null);
return reader.getShapeToMesh();
}

/**
* Reads a 3D mesh from the input stream in the specified format and writes it to the output.
*
Expand Down
110 changes: 93 additions & 17 deletions main/boofcv-io/src/main/java/boofcv/io/points/impl/ObjFileCodec.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import georegression.struct.point.Point2D_F32;
import georegression.struct.point.Point3D_F32;
import georegression.struct.point.Point3D_F64;
import org.apache.commons.io.FilenameUtils;
import org.ddogleg.struct.DogArray_I32;

import java.io.*;
Expand All @@ -44,30 +45,71 @@ public static void save( PointCloudReader cloud, Writer writer ) throws IOExcept
var obj = new ObjFileWriter(writer);
obj.addComment("Created by BoofCV");

boolean hasColor = cloud.colors();

var point = new Point3D_F64();
int N = cloud.size();
for (int i = 0; i < N; i++) {
cloud.get(i, point);
obj.addVertex(point.x, point.y, point.z);
if (hasColor) {
addRgbVertex(obj, point, cloud.getRGB(i));
} else {
obj.addVertex(point.x, point.y, point.z);
}
obj.addPoint(-1);
}
}

/**
* Creates a MTL file. This is required if the mesh is textured mapped
*
* @param textureFile Path to texture mapped file
* @param writer Where the MTL will be written to
*/
public static void saveMtl( String textureFile, Writer writer ) throws IOException {
// Use hard coded values
String text = """
newmtl %s
Ka 1.0 1.0 1.0
Kd 1.0 1.0 1.0
Ks 0.0 0.0 0.0
d 1.0
Ns 0.0
illum 0
map_Kd %s""";

String baseName = FilenameUtils.getBaseName(textureFile);
writer.write(String.format(text, baseName, textureFile));
}

public static void save( VertexMesh mesh, Writer writer ) throws IOException {
var obj = new ObjFileWriter(writer);
obj.addComment("Created by BoofCV");

// If there's a texture file, add a material
if (!mesh.textureName.isEmpty()) {
String baseName = FilenameUtils.getBaseName(mesh.textureName);
obj.addLibrary(baseName + ".mtl");
obj.addMaterial(baseName);
}

boolean hasVertexColors = mesh.rgb.size > 0;

// First save the vertexes
int N = mesh.vertexes.size();
for (int i = 0; i < N; i++) {
Point3D_F64 p = mesh.vertexes.getTemp(i);
obj.addVertex(p.x, p.y, p.z);
if (hasVertexColors) {
addRgbVertex(obj, p, mesh.rgb.get(i));
} else {
obj.addVertex(p.x, p.y, p.z);
}
}

// Save vertex normals
for (int i = 0; i < mesh.normals.size(); i++) {
Point3D_F32 p = mesh.normals.getTemp(i);
obj.addVertex(p.x, p.y, p.z);
obj.addVertexNormal(p.x, p.y, p.z);
}

// Save vertex textures
Expand Down Expand Up @@ -97,8 +139,20 @@ public static void save( VertexMesh mesh, Writer writer ) throws IOException {
}
}

private static void addRgbVertex( ObjFileWriter obj, Point3D_F64 p, int rgb ) throws IOException {
// Convert to float format
double red = ((rgb >> 16) & 0xFF)/255.0;
double green = ((rgb >> 8) & 0xFF)/255.0;
double blue = (rgb & 0xFF)/255.0;
obj.addVertex(p.x, p.y, p.z, red, green, blue);
}

public static void load( InputStream input, PointCloudWriter output ) throws IOException {
var obj = new ObjFileReader() {
@Override protected void addLibrary( String name ) {}

@Override protected void addMaterial( String name ) {}

@Override protected void addVertex( double x, double y, double z ) {
output.startPoint();
output.location(x, y, z);
Expand All @@ -107,10 +161,9 @@ public static void load( InputStream input, PointCloudWriter output ) throws IOE

@Override
protected void addVertexWithColor( double x, double y, double z, double red, double green, double blue ) {
int rgb = ((int)(255*red)) << 16 | ((int)(255*green)) << 8 | ((int)(255*blue));
output.startPoint();
output.location(x, y, z);
output.color(rgb);
output.color(convertToInt(red, green, blue));
output.stopPoint();
}

Expand All @@ -130,15 +183,20 @@ protected void addVertexWithColor( double x, double y, double z, double red, dou
public static void load( InputStream input, VertexMesh output ) throws IOException {
output.reset();
var obj = new ObjFileReader() {
// Library and material doesn't translate well a stream input and mesh output
// A single obj file can define multiple objects with different texture maps
@Override protected void addLibrary( String name ) {}

@Override protected void addMaterial( String name ) {}

@Override protected void addVertex( double x, double y, double z ) {
output.vertexes.append(x, y, z);
}

@Override
protected void addVertexWithColor( double x, double y, double z, double red, double green, double blue ) {
int rgb = ((int)(255*red)) << 16 | ((int)(255*green)) << 8 | ((int)(255*blue));
output.vertexes.append(x, y, z);
output.rgb.add(rgb);
output.rgb.add(convertToInt(red, green, blue));
}

@Override protected void addVertexNormal( double x, double y, double z ) {
Expand All @@ -154,18 +212,36 @@ protected void addVertexWithColor( double x, double y, double z, double red, dou
@Override protected void addLine( DogArray_I32 vertexes ) {}

@Override protected void addFace( DogArray_I32 indexes, int vertexCount ) {
int types = indexes.size/vertexCount;
for (int idxVert = 0; idxVert < vertexCount; idxVert++) {
output.faceVertexes.add(indexes.get(idxVert)/types);
for (int i = 1; i < types; i++) {
if (indexes.get(i - 1) != indexes.get(i))
throw new RuntimeException("Only vertexes types with the same index supported");
}
}

output.faceOffsets.add(output.faceVertexes.size);
addFactToMesh(indexes, vertexCount, output);
}
};
obj.parse(new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8)));
}

static void addFactToMesh( DogArray_I32 indexes, int vertexCount, VertexMesh output ) {
// order of that indexes are specifies is always vertex/texture/normal

boolean hasTexture = output.texture.size() > 0;
boolean hasNormals = output.normals.size() > 0;

int typeCount = indexes.size/vertexCount;
for (int idxVert = 0; idxVert < vertexCount; idxVert++) {
int index = idxVert*typeCount;;
output.faceVertexes.add(indexes.get(index++));

if (hasTexture) {
output.faceVertexTextures.add(indexes.get(index++));
}

if (hasNormals) {
output.faceVertexNormals.add(indexes.get(index));
}
}

output.faceOffsets.add(output.faceVertexes.size);
}

static int convertToInt( double red, double green, double blue ) {
return ((int)(255*red + 0.5)) << 16 | ((int)(255*green + 0.5)) << 8 | ((int)(255*blue + 0.5));
}
}
Loading

0 comments on commit ee21e50

Please sign in to comment.