ARKit LiDAR 扫描导出带纹理 OBJ 模型(JPG+MTL)
2025-03-21 16:37:43
从 ARKit LiDAR 扫描导出带纹理(JPG + MTL) 的 OBJ 模型
在使用 ARKit 和 RealityKit 进行物体扫描时,可以轻松地从 ARMeshAnchors
生成 OBJ 文件。但通常情况下,导出后的模型缺少纹理(JPG + MTL),导致模型看起来单调、缺乏真实感。
问题成因
根本原因在于,ARKit 的 ARMeshAnchor
主要提供的是物体的几何信息(顶点、面等),而不直接包含纹理数据。当前的代码将网格转换为MDLAsset
并导出.obj, 缺少将场景的视觉外观捕获为纹理(通常是 JPG 图像)并将其与 OBJ 模型关联(通过 MTL 文件)的步骤。
解决方案
要解决这个问题,需要手动实现纹理生成和 MTL 文件的创建。 下面给出几种思路和具体实现方式:
1. 利用 ARFrame
的 capturedImage
这是最直接的方法。ARFrame
提供了当前相机捕获的原始图像,可以直接将其保存为 JPG 文件。
原理:
每次 ARFrame
更新时,capturedImage
属性都包含了相机看到的图像数据。 我们可以直接把它转成图片。
步骤:
- 获取
ARFrame
中的capturedImage
。 - 将
CVPixelBuffer
(capturedImage 的类型) 转换为UIImage
。 - 将
UIImage
保存为 JPG 文件。 - 生成mtl文件。
代码示例:
// ... (之前的 exportScannedObject 函数保持不变) ...
func saveCapturedImage(frame: ARFrame) -> URL? {
let pixelBuffer = frame.capturedImage
let ciImage = CIImage(cvPixelBuffer: pixelBuffer)
let context = CIContext()
guard let cgImage = context.createCGImage(ciImage, from: ciImage.extent) else { return nil }
let uiImage = UIImage(cgImage: cgImage)
let directory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
let imageUrl = directory.appendingPathComponent("texture.jpg")
do {
try uiImage.jpegData(compressionQuality: 1.0)?.write(to: imageUrl)
return imageUrl
} catch {
print("Failed to save image: \(error)")
return nil
}
}
private func exportScannedObject() {
// ... 其他代码 ...
guard let frame = arView.session.currentFrame else {return}
if let meshAnchors = frame.anchors.compactMap({ $0 as? ARMeshAnchor }),
let asset = convertToAsset(meshAnchors: meshAnchors) {
do {
let objURL = try export(asset: asset)
//保存图片
if let imageURL = saveCapturedImage(frame: frame){
//生成mtl文件
createMTLFile(objFileName: "scaned", textureFileName: "texture", directory: FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!)
}
showScanPreview(objURL) //展示obj
} catch {
print("export error")
}
}
}
func createMTLFile(objFileName: String, textureFileName: String, directory:URL) {
let mtlText = """
newmtl material_0
Ka 1.000000 1.000000 1.000000
Kd 1.000000 1.000000 1.000000
Ks 0.000000 0.000000 0.000000
Ns 0.000000
map_Kd \(textureFileName).jpg
"""
let mtlURL = directory.appendingPathComponent("\(objFileName).mtl")
do {
try mtlText.write(to: mtlURL, atomically: true, encoding: .utf8)
} catch {
print("Failed to write MTL file: \(error)")
}
}
注意事项:
- 这种方法得到的纹理只是一个视角的图像,可能会导致模型从不同角度看时出现拉伸或变形。
compressionQuality
参数可以调整 JPG 图像的压缩质量(0.0 表示最大压缩,1.0 表示不压缩)。- 上面的代码,只做了单张图片的导出, 如需要多张图片导出, 请自行编写代码循环处理。
2. UV 展开与纹理映射(Texture Mapping)
这是更专业、更准确的方法,但实现起来也更复杂。它涉及 UV 展开和纹理映射两个关键概念。
原理:
- UV 展开: 将三维模型的表面展开成一个二维平面,就像把地球仪展开成世界地图一样。每个顶点都会有一个对应的 UV 坐标(取值范围通常是 0 到 1),用来确定纹理图像上的哪个像素应该应用到这个顶点上。
- 纹理映射: 将一个或多个二维图像(纹理)“贴”到三维模型上的过程。
步骤:
- UV 展开: 为
ARMeshAnchor
生成的网格计算 UV 坐标。这是一个复杂的过程,通常需要借助第三方库或算法。 - 投影相机图像: 将
ARFrame
中的capturedImage
按照 UV 坐标投影到展开的纹理上。 - 保存纹理: 将投影后的纹理保存为 JPG 文件。
- 创建 MTL 文件: 用于告诉渲染引擎哪个obj, 使用哪个 JPG 作为纹理。
进阶技巧:
由于 iOS 上进行 UV 展开较为复杂, 可以利用一些开源或者商业 3D 软件,如 Blender,来处理。步骤如下:
- 将
ARMeshAnchor
导出的 OBJ 文件导入 Blender。 - 在 Blender 中进行 UV 展开,并手动调整 UV 布局,使其尽量不重叠、不拉伸。
- 将多个视角的
capturedImage
作为纹理投影到模型上。在 Blender 中,你可以使用相机投影修改器(Camera Project modifier)或者纹理绘制工具(Texture Paint)来完成这一步。 - 烘焙纹理(Bake Texture):将投影后的纹理合并成一张最终的纹理图像。
- 导出 OBJ、MTL 和 JPG 文件。
注意: 这是一个手动且费力的过程。如果你的应用场景只是为了扫描物体的材质和贴图,可以使用市面上的三维扫描软件。
3. 使用第三方库或服务
有些第三方库或服务可以简化纹理生成的过程。比如:
- RealityKit (仅限 macOS): 如果你在 macOS 上开发,可以考虑使用 RealityKit 的
PhotogrammetrySession
。它可以从一系列图像中生成带纹理的三维模型。 但是,PhotogrammetrySession
并不适用于 iOS 平台. - 第三方服务 : 使用提供LiDAR扫描并生成带纹理模型的服务(可能有费用)。
步骤:
- 参照相应库或服务的文档进行操作。
- 获取包含 OBJ,MTL,和 JPG 文件的结果。
提示 : 搜索相关例如“iOS LiDAR texture mapping SDK" 来获取解决方案.
MTL 文件创建(通用)
无论你选择哪种方法生成纹理,都需要创建一个 MTL 文件来将 OBJ 模型与纹理关联起来。
MTL 文件是一个纯文本文件,其中定义了材质的属性,包括漫反射颜色(Kd)、环境光颜色(Ka)、高光颜色(Ks)、光泽度(Ns)以及纹理贴图(map_Kd)等。
确保mtl 文件里map_Kd
的相对路径指向纹理图像。
安全建议
- 用户隐私: 当你使用相机捕捉图像时,要确保你遵守相关的隐私法规,并在必要时征得用户的同意。
- 文件存储: 如果你将扫描的模型或纹理保存在本地,请注意文件存储的安全性,避免被未经授权的访问。
- 敏感数据: 不要在纹理图中存储敏感信息, 尽量将扫描的内容限制在应用所需的范围内.