如何用 6 个三角形 SCNNode 制作一个六边形?

How do I make a hexagon with 6 triangular SCNNodes?

我试图在不改变任何轴点的情况下用三角形制作六边形网格,但我似乎无法正确定位三角形以制作单个六边形。我正在创建 SCNNodesUIBezierPaths 以形成三角形,然后旋转贝塞尔曲线路径。这似乎工作正常直到我尝试使用参数方程将三角形定位在一个圆周围以形成六边形,然后它们不会在正确的位置结束。你能帮我找出我哪里做错了吗?

class TrianglePlane: SCNNode {

    var size: CGFloat = 0.1
    var coords: SCNVector3 = SCNVector3Zero
    var innerCoords: Int = 0

    init(coords: SCNVector3, innerCoords: Int, identifier: Int) {
        super.init()

        self.coords = coords
        self.innerCoords = innerCoords
        setup()
    }

    init(identifier: Int) {
        super.init()
//        super.init(identifier: identifier)
        setup()
    }

    required init?(coder aDecoder: NSCoder) { 
        fatalError("init(coder:) has not been implemented") 
    }

    func setup() {
        let myPath = path()
        let geo = SCNShape(path: myPath, extrusionDepth: 0)
        geo.firstMaterial?.diffuse.contents = UIColor.red
        geo.firstMaterial?.blendMode = .multiply
        self.geometry = geo
    }

    func path() -> UIBezierPath {

        let max: CGFloat = self.size
        let min: CGFloat = 0

        let bPath = UIBezierPath()
        bPath.move(to: .zero)
        bPath.addLine(to: CGPoint(x: max / 2, 
                                  y: UIBezierPath.middlePeak(height: max)))
        bPath.addLine(to: CGPoint(x: max, y: min))
        bPath.close()
        return bPath
    }
}

extension TrianglePlane {

    static func generateHexagon() -> [TrianglePlane] {

        var myArr: [TrianglePlane] = []

        let colors = [UIColor.red, UIColor.green, 
                      UIColor.yellow, UIColor.systemTeal, 
                      UIColor.cyan, UIColor.magenta]


        for i in 0 ..< 6  {

            let tri = TrianglePlane(identifier: 0)
            tri.geometry?.firstMaterial?.diffuse.contents = colors[i]
            tri.position = SCNVector3( -0.05, 0, -0.5)

//          Rotate bezier path
            let angleInDegrees = (Float(i) + 1) * 180.0
            print(angleInDegrees)
            let angle = CGFloat(deg2rad(angleInDegrees))
            let geo = tri.geometry as! SCNShape
            let path = geo.path!
            path.rotateAroundCenter(angle: angle)
            geo.path = path

//          Position triangle in hexagon
            let radius = Float(tri.size)/2
            let deg: Float = Float(i) * 60
            let radians = deg2rad(-deg)

            let x1 = tri.position.x + radius * cos(radians)
            let y1 = tri.position.y + radius * sin(radians)
            tri.position.x = x1
            tri.position.y = y1

            myArr.append(tri)
        }

        return myArr
    }

    static func deg2rad(_ number: Float) -> Float {
        return number * Float.pi / 180
    }
}

extension UIBezierPath {

    func rotateAroundCenter(angle: CGFloat) {

        let center = self.bounds.center
        var transform = CGAffineTransform.identity
        transform = transform.translatedBy(x: center.x, y: center.y)
        transform = transform.rotated(by: angle)
        transform = transform.translatedBy(x: -center.x, y: -center.y)
        self.apply(transform)
    }

    static func middlePeak(height: CGFloat) -> CGFloat {
        return sqrt(3.0) / 2 * height
    }
}

extension CGRect {
    var center : CGPoint {
        return CGPoint(x:self.midX, y:self.midY)
    }
}

What it currently looks like:

What it SHOULD look like:

我创建了两个版本——SceneKit 和 RealityKit。


SceneKit(macOS 版本)

构成六边形的最简单方法是使用六个非均匀缩放的 SCNPyramids(平面)及其移动的轴心点。每个“三角形”必须以 60 度为增量旋转 (.pi/3)。

import SceneKit

class ViewController: NSViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let sceneView = self.view as! SCNView
        let scene = SCNScene()
        sceneView.scene = scene
        sceneView.allowsCameraControl = true
        sceneView.backgroundColor = NSColor.white

        let cameraNode = SCNNode()
        cameraNode.camera = SCNCamera()
        scene.rootNode.addChildNode(cameraNode)
        cameraNode.position = SCNVector3(x: 0, y: 0, z: 15)
        
        for i in 1...6 {

            let triangleNode = SCNNode(geometry: SCNPyramid(width: 1.15,
                                                           height: 1,
                                                           length: 1))

            // the depth of the pyramid is almost zero
            triangleNode.scale = SCNVector3(5, 5, 0.001)

            // move a pivot point from pyramid its base to upper vertex
            triangleNode.simdPivot.columns.3.y = 1     

            triangleNode.geometry?.firstMaterial?.diffuse.contents = NSColor(
                                                 calibratedHue: CGFloat(i)/6,
                                                    saturation: 1.0, 
                                                    brightness: 1.0, 
                                                         alpha: 1.0)

            triangleNode.rotation = SCNVector4(0, 0, 1, 
                                              -CGFloat.pi/3 * CGFloat(i))

            scene.rootNode.addChildNode(triangleNode)
        }
    }
}


RealityKit(iOS 版本)

在这个项目中,我借助 MeshDescriptor 生成了一个三角形,并将其复制了 5 次。

import UIKit
import RealityKit

class ViewController: UIViewController {

    @IBOutlet var arView: ARView!
    let anchor = AnchorEntity()
    let camera = PointOfView()
    let indices: [UInt32] = [0, 1, 2]

    override func viewDidLoad() {
        super.viewDidLoad()
        
        self.arView.environment.background = .color(.black)
        self.arView.cameraMode = .nonAR
        self.camera.position.z = 9

        let positions: [simd_float3] = [[ 0.00, 0.00, 0.00],
                                        [ 0.52, 0.90, 0.00],
                                        [-0.52, 0.90, 0.00]]
         
        var descriptor = MeshDescriptor(name: "Hexagon's side")
        descriptor.materials = .perFace(self.indices)
        descriptor.primitives = .triangles(self.indices)
        descriptor.positions = MeshBuffers.Positions(positions[0...2])
        
        var material = UnlitMaterial()
        let mesh: MeshResource = try! .generate(from: [descriptor])
        
        let colors: [UIColor] = [.systemRed, .systemGreen, .yellow,
                                 .systemTeal, .cyan, .magenta]
        
        for i in 0...5 {

            material.color = .init(tint: colors[i], texture: nil)
            let triangleModel = ModelEntity(mesh: mesh, 
                                       materials: [material])

            let trianglePivot = Entity()    // made to control pivot point
            trianglePivot.addChild(triangleModel)
            
            trianglePivot.orientation = simd_quatf(angle: -.pi/3 * Float(i),
                                                    axis: [0,0,1])
            self.anchor.addChild(trianglePivot)
        }
        
        self.anchor.addChild(self.camera)
        self.arView.scene.anchors.append(self.anchor)
    }
}

目前的代码存在一些问题。首先,正如评论中指出的那样,平移的参数方程需要旋转 90 度:

        let deg: Float = (Float(i) * 60) - 90.0

下一个问题是三角形的边界框的中心和三角形的质心不是同一个点。这很重要,因为参数方程计算三角形的质心必须位于的位置,而不是它们的边界框的中心。所以我们需要一种方法来计算质心。这可以通过向 TrianglePlane 添加以下扩展方法来完成:

extension TrianglePlane {
    /// Calculates the centroid of the triangle
    func centroid() -> CGPoint
    {
        let max: CGFloat = self.size
        let min: CGFloat = 0
        let peak = UIBezierPath.middlePeak(height: max)
        let xAvg = (min + max / CGFloat(2.0) + max) / CGFloat(3.0)
        let yAvg = (min + peak + min) / CGFloat(3.0)
        return CGPoint(x: xAvg,  y: yAvg)
    }
}

这允许计算参数方程的正确 radius

        let height = Float(UIBezierPath.middlePeak(height: tri.size))
        let centroid = tri.centroid()
        let radius = height - Float(centroid.y)

最后的校正是计算三角形原点和质心之间的偏移量。此修正取决于三角形是否已被旋转翻转:

        let x1 =  radius * cos(radians)
        let y1 =  radius * sin(radians)
        let dx = Float(-centroid.x)
        let dy = (i % 2 == 0) ? Float(centroid.y) - height  : Float(-centroid.y)
        tri.position.x = x1 + dx
        tri.position.y = y1 + dy

将所有这些放在一起可以得到预期的结果。

完整工作 ViewController 可以在此 gist

中找到

注意将三角形的原点设为质心可以大大简化代码