Hi @JeffCloe,
Forgive me if this gets lengthy, but I'll try to stick to the key points on how I achieved success with this task. @gchiste's help pointed me in all of the right directions, and while I'm certainly an ARKit/point cloud novice, that help taught me quite a bit. To keep the reply as brief as possible, I will assume that you have the
Visualizing a Point Cloud using Scene Depth project already downloaded and accessible.
Firstly, you'll need to have some methodology to tell the app when you are done "scanning" the environment, and to save the point clouds to a file. For ease, I added a simple
UIButton to my
ViewController.swift's
viewDidLoad method;
Code Block // Setup a save button |
let button = UIButton(type: .system, primaryAction: UIAction(title: "Save", handler: { (action) in |
self.renderer.savePointsToFile() |
})) |
|
button.translatesAutoresizingMaskIntoConstraints = false |
self.view.addSubview(button) |
NSLayoutConstraint.activate([ |
button.centerXAnchor.constraint(equalTo: self.view.centerXAnchor), |
button.centerYAnchor.constraint(equalTo: self.view.centerYAnchor) |
]) |
Naturally, in your
Renderer.swift, you'll need to add a new method to handle when the button is tapped. Additionally, you'll likely want to add a variable to your
Renderer.swift file, something like,
var isSavingFile = false, to prevent the Save button from being tapped repeatedly while a save is in process. More importantly, setting up your
savePointsToFile() method in
Renderer.swift, is where the bulk of the work takes place.
Code Block private func savePointsToFile() { |
guard !self.isSavingFile else { return } |
self.isSavingFile = true |
|
// 1 |
var fileToWrite = "" |
let headers = ["ply", "format ascii 1.0", "element vertex \(currentPointCount)", "property float x", "property float y", "property float z", "property uchar red", "property uchar green", "property uchar blue", "property uchar alpha", "element face 0", "property list uchar int vertex_indices", "end_header"] |
for header in headers { |
fileToWrite += header |
fileToWrite += "\r\n" |
} |
|
// 2 |
for i in 0..<currentPointCount { |
|
// 3 |
let point = particlesBuffer[i] |
let colors = point.color |
|
// 4 |
let red = colors.x * 255.0 |
let green = colors.y * 255.0 |
let blue = colors.z * 255.0 |
|
// 5 |
let pvValue = "\(point.position.x) \(point.position.y) \(point.position.z) \(Int(red)) \(Int(green)) \(Int(blue)) 255" |
fileToWrite += pvValue |
fileToWrite += "\r\n" |
} |
|
// 6 |
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask) |
let documentsDirectory = paths[0] |
let file = documentsDirectory.appendingPathComponent("ply_\(UUID().uuidString).ply") |
|
do { |
|
// 7 |
try fileToWrite.write(to: file, atomically: true, encoding: String.Encoding.ascii) |
self.isSavingFile = false |
} catch { |
print("Failed to write PLY file", error) |
} |
} |
Going through the method, I'll try to detail my approach broken down by notation;
1) A .ply file, as I recently learned, requires a header detailing the file format, number of vertex, and formats for the points x, y, z parameters, as well as color parameters. In this case, since we are using only points and not a mesh, the header indicates there will be no faces to the model, and we end our header with a notation that the index of each vertex is an integer. As a whole, it's worth mentioning that I am effectively just creating a "text" file, with each line (that isn't the header) being the details for each point, and then saving that file out with a .ply extension.
2) Using the
currentPointCount, which is already being calculated and incremented through the sample project, I am iterating from 0 through the number of collected points.
3) Using the index, I am accessing the relevant point through the
particlesBuffer, which provides me the point as a
ParticleUniforms. This gives me access to the relevant point data, which includes the point's X, Y, and Z position, as well as the RGB color values.
4) I am setting up the colors as its own item, then multiplying the Red, Green, and Blue by 255 to get the relevant RGB color. The color data is saved as a
simd_float_3, which sets each color value to X, Y, Z components (red is X, green is Y, blue is Z).
5) Creating a string with the data formatted as the .ply file expects allows it to be written to be appended to the existing
fileToWrite, which already contains our header. After some trial and error, I found this syntax here created the best result (in this case, converting the RGB values from Floats to Ints, which rounds them). The last column indicates the alpha value of the point, which I am setting to 255, as each pixel should be fully visible. The
pvValue string is appended to
fileTowrite, as is a return carriage so the next point is added to the subsequent line.
6) Once all of the points have been added to
fileToWrite, I am setting up a file path/file name as to where I want to write the file.
7) Finally, the file is being written to my desired destination. At this point, you could decide what you want to do with the file, whether that's to provide the user an option to save/share, upload it somewhere, etc. I'm setting my
isSavingFile to false, and that's the setup. Once I grab my saved file (in my case, I provide the user a
UIActivityController to save/share the file), and preview it (I'm using Meshlab on my Mac for preview purposes), I see the rendered point cloud. I've also tried uploading to SketchFab and it seems to work well.
A few notes;
My end goal is to save the point cloud as a .usdz file, not necessarily a .ply. @gchiste pointed me in the right direction, by creating something like a SCNSphere and coloring its material's diffuse contents with the relevant point cloud color, then setting its X, Y, and Z position in my SceneKit's view coordinate space. I did manage to get a SceneKit representation of the point cloud working, but the app crashes when I try to save out as a .usdz, with no particular indication as to why it's crashing. I filed feedback on this issue.
The PLY file generated can be quite large depending on how many points you've gathered. While I'm a novice at PLY and modeling, I believe that writing the PLY file in a different format, such as working with little endian or big endian encoding, could yield smaller PLY file results. I haven't figured that out yet, but I saw an app in the App Store that seems to gather the point clouds/generate a PLY file, and the resulting PLY is in little endian format, and much smaller in file size. Just worth mentioning.
This does not at all account of performance (which, there might be more efficient ways of doing this), nor providing the user any feedback that file writing is taking place, which can be time consuming. If you're planning to use this in a production app, just things to consider.