如何在放置虚拟物体前用ARKit、SceneKit测量水平面并生成房间模型?
Hey there! Let's walk through how to implement horizontal plane measurement and room 3D modeling using ARKit and SceneKit—this is a super common AR use case, so I’ll break it down into actionable, easy-to-follow steps.
First, we need to get ARKit to detect horizontal surfaces, then layer on the measurement logic.
1. Configure the AR Session
Start by setting up your ARView and enabling horizontal plane detection. This tells ARKit to actively look for flat surfaces like floors or tables:
import ARKit import SceneKit class ARViewController: UIViewController, ARSessionDelegate { @IBOutlet weak var arView: ARView! override func viewDidLoad() { super.viewDidLoad() arView.session.delegate = self let configuration = ARWorldTrackingConfiguration() // Enable horizontal plane detection configuration.planeDetection = .horizontal // Use gravity + heading for a stable, real-world-aligned coordinate system configuration.worldAlignment = .gravityAndHeading arView.session.run(configuration) } }
2. Visualize Detected Planes
Once ARKit finds a plane, we’ll render a semi-transparent surface so the user can see exactly where the detected plane is. Add this delegate method to handle new plane anchors:
func session(_ session: ARSession, didAdd anchors: [ARAnchor]) { guard let planeAnchor = anchors.first as? ARPlaneAnchor else { return } // Create a SceneKit plane matching the detected plane's dimensions let planeNode = SCNNode() let planeGeometry = SCNPlane(width: CGFloat(planeAnchor.extent.x), height: CGFloat(planeAnchor.extent.z)) planeGeometry.firstMaterial?.diffuse.contents = UIColor.blue.withAlphaComponent(0.3) // Rotate the plane to lie flat (SCNPlane is vertical by default) planeNode.geometry = planeGeometry planeNode.position = SCNVector3(planeAnchor.center.x, 0, planeAnchor.center.z) planeNode.eulerAngles.x = -.pi / 2 // Add the visualized plane to the AR scene arView.scene.rootNode.addChildNode(planeNode) }
3. Implement Measurement Logic
To measure distances between two points on the plane, we’ll track user taps to record start and end points:
- First, add variables to store the start point and a placeholder for measurement visualization nodes:
private var startPoint: SCNVector3? private var measurementVisualizationNodes: [SCNNode] = []
- Add a tap gesture handler to capture user input:
@objc func handleTap(_ gesture: UITapGestureRecognizer) { let location = gesture.location(in: arView) // Perform a hit test to find the tapped point on a detected plane guard let hitResult = arView.hitTest(location, types: .existingPlaneUsingExtent).first else { print("No plane detected at tap location—try moving your device to scan more area") return } let worldPoint = hitResult.worldTransform.columns.3 let tappedPoint = SCNVector3(worldPoint.x, worldPoint.y, worldPoint.z) if startPoint == nil { // First tap: set start point and place a red marker startPoint = tappedPoint let markerNode = SCNNode(geometry: SCNSphere(radius: 0.01)) markerNode.geometry?.firstMaterial?.diffuse.contents = UIColor.red markerNode.position = tappedPoint arView.scene.rootNode.addChildNode(markerNode) measurementVisualizationNodes.append(markerNode) } else { // Second tap: calculate distance and display it guard let start = startPoint else { return } // Calculate 2D distance on the horizontal plane (ignore Y-axis) let distance = sqrt(pow(tappedPoint.x - start.x, 2) + pow(tappedPoint.z - start.z, 2)) // Draw a line between start and end points let lineNode = SCNNode(geometry: createLine(from: start, to: tappedPoint)) arView.scene.rootNode.addChildNode(lineNode) measurementVisualizationNodes.append(lineNode) // Display formatted distance text let textGeometry = SCNText(string: String(format: "%.2f m", distance), extrusionDepth: 0.01) textGeometry.firstMaterial?.diffuse.contents = UIColor.white let textNode = SCNNode(geometry: textGeometry) // Position text halfway between the two points, lifted slightly off the plane textNode.position = SCNVector3( (start.x + tappedPoint.x)/2, start.y + 0.1, (start.z + tappedPoint.z)/2 ) textNode.scale = SCNVector3(0.01, 0.01, 0.01) // Scale down the text to fit arView.scene.rootNode.addChildNode(textNode) measurementVisualizationNodes.append(textNode) // Reset start point for next measurement startPoint = nil } } // Helper method to create a line between two 3D points private func createLine(from start: SCNVector3, to end: SCNVector3) -> SCNGeometry { let positions = [start, end] let source = SCNGeometrySource(vertices: positions) let indices: [Int32] = [0, 1] let element = SCNGeometryElement(indices: indices, primitiveType: .line) let line = SCNGeometry(sources: [source], elements: [element]) line.firstMaterial?.diffuse.contents = UIColor.white return line }
- Don’t forget to add the tap gesture recognizer in
viewDidLoad():
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:))) arView.addGestureRecognizer(tapGesture)
For generating a full room model, we’ll use ARKit’s mesh reconstruction feature (available in ARKit 3.0+) to capture the environment’s 3D structure, then refine it into a clean, structured room model.
1. Enable Mesh Reconstruction
Switch to ARMeshConfiguration to let ARKit generate a detailed 3D mesh of the room:
let meshConfig = ARMeshConfiguration() meshConfig.isLightEstimationEnabled = true // Reset previous tracking to start fresh with mesh scanning arView.session.run(meshConfig, options: [.resetTracking, .removeExistingAnchors])
2. Visualize the Scanned Mesh
Render the generated mesh so the user can see the scanned environment in real time:
func session(_ session: ARSession, didAdd anchors: [ARAnchor]) { guard let meshAnchor = anchors.first as? ARMeshAnchor else { return } // Create SceneKit geometry from the mesh anchor's data let meshGeometry = ARSCNGeometry(meshAnchor: meshAnchor) let meshNode = SCNNode(geometry: meshGeometry) // Set a semi-transparent green material for the mesh let material = SCNMaterial() material.diffuse.contents = UIColor.green.withAlphaComponent(0.5) meshGeometry.materials = [material] arView.scene.rootNode.addChildNode(meshNode) }
3. Generate a Structured Room Model
To turn the raw mesh into a clean, usable room model (walls, floor, ceiling), you’ll need to:
- Merge adjacent planes: If you enable vertical plane detection (set
configuration.planeDetection = [.horizontal, .vertical]), useARPlaneAnchordata to identify large, flat surfaces. Check thealignmentproperty to distinguish horizontal (floor/ceiling) vs vertical (walls) planes. - Extract boundaries: For each detected plane, use the
extentandcenterproperties to define edges, then createSCNPlaneorSCNBoxnodes to represent the room’s structure. - Clean up noise: Filter out small, irrelevant mesh fragments to keep the model tidy (you can use threshold checks on plane size to exclude tiny surfaces).
4. Export the Room Model
Once you’ve built the structured room model, export it as a USDZ file (Apple’s preferred AR format) for sharing or further editing:
func exportRoomModel() { guard let scene = arView.scene else { return } let documentsDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! let exportURL = documentsDir.appendingPathComponent("RoomModel.usdz") do { try scene.write(to: exportURL, options: nil, delegate: nil, progressHandler: nil) print("Model successfully exported to \(exportURL)") } catch { print("Export failed: \(error.localizedDescription)") } }
- Improve detection accuracy: Move the device slowly around the room to let ARKit capture more detail. Pause briefly over surfaces to refine plane detection.
- Reduce measurement error: Perform multiple hit tests at the tap location and average the results to minimize inaccuracies from camera noise.
- Optimize mesh rendering: On older devices, lower the mesh detail level or use occlusion to hide parts of the mesh that aren’t visible to the user.
- Guide the user: Add on-screen prompts like "Slowly scan your room to detect walls and floors" to help users get better, more complete scans.
Hope these steps help you build your AR measurement and room modeling feature smoothly! If you run into specific code bugs or edge cases (like handling curved walls), feel free to share more details.
内容的提问来源于stack exchange,提问作者Ancuta Cozma

