You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

ARKit中将ARAnchor转为带纹理SCNGeometry时纹理拉伸问题

解决ARKit WorldTracking中锚点更新时纹理拉伸问题

核心问题

每次更新ARMeshAnchor时,直接用当前相机图像覆盖整个网格纹理,导致不在当前视野内的网格区域因错误映射出现拉伸。以下是几种可行的解决方法:


方法一:仅对视野内的锚点更新纹理

通过判断锚点是否在相机视锥体内,决定是否更新纹理,视野外的锚点保留原有材质。

实现步骤

  1. 添加锚点可见性判断函数:
func isAnchorVisible(anchor: ARMeshAnchor, camera: ARCamera) -> Bool {
    // 获取锚点世界坐标
    let anchorWorldPos = SIMD3<Float>(anchor.transform.columns.3.x, 
                                      anchor.transform.columns.3.y, 
                                      anchor.transform.columns.3.z)
    // 转换到相机空间
    let cameraSpacePos = camera.transform.inverse * SIMD4<Float>(anchorWorldPos, 1)
    // 排除相机后方的锚点
    guard cameraSpacePos.z < 0 else { return false }
    // 转换到NDC空间(标准化设备坐标)
    let ndcPos = camera.projectionMatrix * cameraSpacePos
    // 判断是否在视锥体范围内
    return abs(ndcPos.x) <= 1 && abs(ndcPos.y) <= 1
}
  1. 修改更新锚点的回调:
func renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor) {
    guard let frame = self.sceneView.session.currentFrame else { return }
    guard let meshAnchor = anchor as? ARMeshAnchor else { return }
    
    let camera = frame.camera
    // 判断锚点是否在当前视野内
    guard isAnchorVisible(anchor: meshAnchor, camera: camera) else {
        // 视野外锚点不更新纹理,保留原有材质
        let geometry = self.scanUpdatedGeometory(frame: frame, anchor: meshAnchor, node: node, needTexture: false)
        node.geometry = geometry
        return
    }
    
    // 视野内锚点更新纹理
    let geometry = self.scanUpdatedGeometory(frame: frame, anchor: meshAnchor, node: node, needTexture: true, cameraImage: captureCamera())
    node.geometry = geometry
}

方法二:为顶点生成正确的纹理坐标(彻底解决拉伸)

问题根源是纹理未正确映射到每个顶点,导致整个网格强行适配单张图像。通过为每个顶点计算对应相机图像的UV坐标,实现精准纹理映射。

实现步骤

  1. 扩展SCNGeometry,支持从ARMeshGeometry生成带正确UV的几何体:
extension SCNGeometry {
    convenience init(arMeshGeometry: ARMeshGeometry, camera: ARCamera, modelMatrix: simd_float4x4, needTexture: Bool) {
        let vertices = arMeshGeometry.vertices
        let normals = arMeshGeometry.normals
        let faces = arMeshGeometry.faces
        
        // 创建顶点数据源
        let vertexSource = SCNGeometrySource(buffer: vertices.buffer, 
                                             vertexFormat: vertices.format, 
                                             semantic: .vertex, 
                                             vertexCount: vertices.count, 
                                             dataOffset: vertices.offset, 
                                             dataStride: vertices.stride)
        
        // 创建法线数据源
        let normalSource = SCNGeometrySource(buffer: normals.buffer, 
                                            vertexFormat: normals.format, 
                                            semantic: .normal, 
                                            vertexCount: normals.count, 
                                            dataOffset: normals.offset, 
                                            dataStride: normals.stride)
        
        // 生成UV坐标数据
        var uvData = Data()
        if needTexture {
            for index in 0..<vertices.count {
                let vertex = vertices[index]
                // 顶点从局部空间转世界空间
                let worldVertex = modelMatrix * simd_float4(vertex, 1)
                // 世界空间转相机空间
                let cameraVertex = camera.transform.inverse * worldVertex
                // 相机空间转NDC空间
                let ndcVertex = camera.projectionMatrix * cameraVertex
                // NDC坐标转UV(转换为0-1范围)
                let uv = simd_float2((ndcVertex.x + 1)/2, (1 - ndcVertex.y)/2)
                uvData.append(Data(bytes: &uv, count: MemoryLayout<simd_float2>.stride))
            }
        }
        
        // 创建UV数据源
        let uvSource = SCNGeometrySource(data: uvData, 
                                        semantic: .texcoord, 
                                        vectorCount: vertices.count, 
                                        usesFloatComponents: true, 
                                        componentsPerVector: 2, 
                                        bytesPerComponent: MemoryLayout<Float>.stride, 
                                        dataOffset: 0, 
                                        dataStride: MemoryLayout<simd_float2>.stride)
        
        // 创建面元素
        let faceData = Data(buffer: faces.buffer)
        let faceElement = SCNGeometryElement(data: faceData, 
                                            primitiveType: .triangles, 
                                            primitiveCount: faces.count, 
                                            bytesPerIndex: faces.indexCountPerPrimitive == 3 ? MemoryLayout<UInt32>.stride : MemoryLayout<UInt16>.stride)
        
        self.init(sources: [vertexSource, normalSource, uvSource], elements: [faceElement])
    }
}
  1. 修改纹理应用函数,添加纹理包裹模式避免拉伸:
func scanGeometory(frame: ARFrame, anchor: ARMeshAnchor, node: SCNNode, needTexture: Bool = false, cameraImage: UIImage? = nil) -> SCNGeometry {
    let camera = frame.camera
    let geometry = SCNGeometry(arMeshGeometry: anchor.geometry, camera: camera, modelMatrix: anchor.transform, needTexture: needTexture)
    
    if let image = cameraImage, needTexture {
        geometry.firstMaterial?.diffuse.contents = image
        // 设置纹理包裹模式为clamp,超出UV范围的区域使用边缘颜色,避免拉伸
        geometry.firstMaterial?.diffuse.wrapS = .clamp
        geometry.firstMaterial?.diffuse.wrapT = .clamp
    } else {
        geometry.firstMaterial?.diffuse.contents = UIColor(red: 0.5, green: 1.0, blue: 0.0, alpha: 0.7)
    }
    node.geometry = geometry
    return geometry
}

方法三:多纹理叠加(进阶方案)

如果需要保留不同视角的纹理信息,可以维护一个纹理缓存,记录每次扫描的图像,然后根据顶点的视角方向选择对应的纹理采样。这种方案适合需要更完整纹理重建的场景,但实现复杂度较高,需结合顶点颜色或额外的UV通道来区分不同视角的区域。


内容的提问来源于stack exchange,提问作者Zia ur Rehman

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.19 22:45:16