ARKit SceneView中如何增加线条厚度并实现虚线效果?
解决方案:ARKit中实现加粗线条与虚线样式
我之前也踩过这个坑!glLineWidth()在SceneKit里基本起不到作用——一方面iOS默认用Metal渲染器,OpenGL的API不兼容;另一方面就算用OpenGL ES,它也是全局设置,没法单独控制某条线条的宽度,还受硬件限制(很多设备最大只能支持到10左右)。下面给你两种需求的可靠实现方案:
一、实现加粗线条:用几何体模拟
最稳妥的方式是用**SCNCylinder(圆柱体)**或者SCNPlane(平面)来模拟粗线条,这样不仅能自由控制宽度,还能在3D空间里正确显示。这里推荐用圆柱体,适合ARKit的3D场景:
代码实现:
func createThickLine(from start: SCNVector3, to end: SCNVector3, lineWidth: CGFloat = 0.01) -> SCNNode { // 计算两点间的向量和长度 let vector = end - start let lineLength = vector.length() // 创建圆柱体:半径是线条宽度的一半,高度是两点间距离 let cylinder = SCNCylinder(radius: lineWidth / 2, height: lineLength) cylinder.radialSegmentCount = 16 // 数值越高,圆柱越平滑(按需调整) // 创建线条节点 let lineNode = SCNNode(geometry: cylinder) // 将节点定位在两点的中点 lineNode.position = SCNVector3( (start.x + end.x) / 2, (start.y + end.y) / 2, (start.z + end.z) / 2 ) // 旋转圆柱,使其对齐两点的方向 let direction = vector.normalized() let upVector = SCNVector3(0, 1, 0) let rotationAxis = upVector.cross(direction) let rotationAngle = acos(upVector.dot(direction)) lineNode.transform = SCNMatrix4MakeRotation(rotationAngle, rotationAxis.x, rotationAxis.y, rotationAxis.z) return lineNode } // 给SCNVector3扩展必要的计算方法 extension SCNVector3 { // 计算向量长度 func length() -> CGFloat { return CGFloat(sqrt(x*x + y*y + z*z)) } // 归一化向量(转为单位向量) func normalized() -> SCNVector3 { let len = length() return SCNVector3(x/CGFloat(len), y/CGFloat(len), z/CGFloat(len)) } // 向量减法 static func -(lhs: SCNVector3, rhs: SCNVector3) -> SCNVector3 { return SCNVector3(lhs.x - rhs.x, lhs.y - rhs.y, lhs.z - rhs.z) } // 叉乘(计算旋转轴) func cross(_ vector: SCNVector3) -> SCNVector3 { return SCNVector3( y * vector.z - z * vector.y, z * vector.x - x * vector.z, x * vector.y - y * vector.x ) } // 点乘(计算旋转角度) func dot(_ vector: SCNVector3) -> Float { return x * vector.x + y * vector.y + z * vector.z } }
使用时直接调用createThickLine(from: pos1, to: pos2, lineWidth: 0.02)就能得到你想要的粗线条了。
二、实现虚线样式
有两种常用方案,按需选择:
方案1:用纹理图片实现(简单易上手)
创建一张虚线纹理图,然后将其贴到我们刚才的圆柱体上,通过调整纹理重复参数实现虚线效果:
func createDashedLineWithTexture(from start: SCNVector3, to end: SCNVector3, lineWidth: CGFloat = 0.01, dashLength: CGFloat = 0.1, gapLength: CGFloat = 0.05) -> SCNNode { let lineNode = createThickLine(from: start, to: end, lineWidth: lineWidth) // 生成虚线纹理(也可以直接用本地图片) let textureSize = CGSize(width: Int((dashLength + gapLength) * 10), height: Int(lineWidth * 1000)) UIGraphicsBeginImageContext(textureSize) defer { UIGraphicsEndImageContext() } let context = UIGraphicsGetCurrentContext()! // 透明背景 context.setFillColor(UIColor.clear.cgColor) context.fill(CGRect(origin: .zero, size: textureSize)) // 绘制虚线片段 context.setStrokeColor(UIColor.black.cgColor) context.setLineWidth(lineWidth * 1000) context.move(to: CGPoint(x: 0, y: textureSize.height/2)) context.addLine(to: CGPoint(x: dashLength * 10, y: textureSize.height/2)) context.strokePath() let dashedImage = UIGraphicsGetImageFromCurrentImageContext()! // 设置材质 let material = SCNMaterial() material.diffuse.contents = dashedImage // X方向重复纹理,Y方向固定 material.diffuse.wrapS = .repeat material.diffuse.wrapT = .clamp // 调整纹理缩放,让虚线沿线条长度正确重复 let lineLength = (end - start).length() material.diffuse.contentsTransform = SCNMatrix4MakeScale(Float(lineLength / (dashLength + gapLength)), 1, 1) lineNode.geometry?.materials = [material] return lineNode }
方案2:用自定义着色器实现(灵活可控)
如果需要动态调整虚线的长度、间距,或者不想依赖图片,可以用SceneKit的ShaderModifier来实现:
func createDashedLineWithShader(from start: SCNVector3, to end: SCNVector3, lineWidth: CGFloat = 0.01, dashLength: Float = 0.1, gapLength: Float = 0.05) -> SCNNode { let lineNode = createThickLine(from: start, to: end, lineWidth: lineWidth) // 片段着色器修改器:计算虚线的透明度 let shaderModifier = """ #pragma transparent varying vec2 v_texCoord; void main() { float dashTotal = \(dashLength) + \(gapLength); float position = v_texCoord.x * dashTotal; // 计算当前位置是否在虚线片段内 float alpha = step(fract(position / dashTotal), \(dashLength)/dashTotal); gl_FragColor = vec4(gl_FragColor.rgb, alpha); } """ let material = SCNMaterial() material.diffuse.contents = UIColor.black material.shaderModifiers = [.fragment: shaderModifier] // 开启透明混合,确保虚线间隙透明 material.isDoubleSided = true material.blendMode = .alpha lineNode.geometry?.materials = [material] // 给圆柱体添加纹理坐标,让着色器能获取沿线条长度的位置信息 let cylinder = lineNode.geometry as! SCNCylinder let texCoords = [ SCNVector2(0, 0), SCNVector2(1, 0), SCNVector2(0, 1), SCNVector2(1, 1) ] let texSource = SCNGeometrySource(textureCoordinates: texCoords) cylinder.sources = cylinder.sources + [texSource] return lineNode }
总结
- 放弃
glLineWidth(),用几何体模拟粗线条是最可靠的方案,适配所有SceneKit渲染环境。 - 虚线样式可以根据需求选择纹理或着色器方案:纹理适合静态场景,着色器适合需要动态调整参数的场景。
内容的提问来源于stack exchange,提问作者Math_Enthusiast
相关产品推荐
相关产品推荐

