SCNShape 不为 NSBezierPath 绘制形状

SCNShape doesn't draw a shape for NSBezierPath

我遇到了一些 NSBezierPaths SCNShape 似乎无法绘制形状。

路径仅使用 line(to:) 创建。

//...set up scene...

//Create path (working)
let path = NSBezierPath()
path.move(to: CGPoint.zero)
path.line(to: NSMakePoint(0.000000, 0.000000))
path.line(to: NSMakePoint(0.011681, 0.029526))
// more points ...
path.close()

// Make a 3D shape (not working)
let shape = SCNShape(path: path, extrusionDepth: 10)
shape.firstMaterial?.diffuse.contents = NSColor.green
let node = SCNNode(geometry: shape)
root.addChildNode(node)

为了验证创建SCNShape的大致过程是正确的,我还画了一个蓝色的形状,只是点不同而已。绘制蓝色形状,绿色形状不绘制。

您可以找到包含完整示例的游乐场 here。在示例中,您应该能够在助理编辑器中看到一个绿色和一个蓝色的形状。但是只绘制了蓝色形状。

你知道为什么没有显示绿色形状吗?

简短的故事:你的路径上的点比它需要的多得多,导致你遇到意想不到的、难以找到的几何问题。

注意 documentation 中的这一点:

The result of extruding a self-intersecting path is undefined.

事实证明,在前 8 个左右的点中,您的 "curve" 以错误的方式转向了关闭路径的线(在路径的第一个点之间 0,0,最后一点 32.366829, 29.713470) 与路径的其余部分相交。这是通过从游乐场渲染中排除除前几个点和最后一个点以外的所有点来使其可见的尝试(请参阅左下角的小锯齿形):

并且至少在某些 SceneKit versions/renderers 上,当它试图从自相交路径中创建网格时,它只是放弃并且什么也没做。

然而,你真的不需要那么多点来让你的路径看起来不错。这是如果你使用 1x、1/5x 和 1/10x 的点数:

如果您总体上排除了足够多的点,and/or 跳过开头的几个使您的曲线在应该弯曲的地方弯曲的点,SceneKit 会很好地呈现形状:


诊断问题的一些技巧:

当像这样处理大量坐标数据时,我喜欢使用 ExpressibleByArrayLiteral 这样我就可以轻松构建一个包含大量 points/vectors/etc:

的数组
extension CGPoint: ExpressibleByArrayLiteral {
    public init(arrayLiteral elements: CGFloat...) {
        precondition(elements.count == 2)
        self.init(x: elements.first!, y: elements.last!)
    }
}

var points: [CGPoint] = [
    [0.000000, 0.000000],
    [0.011681, 0.029526],
    // ...
]

这让我得到了一个数组(并且更少地输入像 NSPointMake 这样的东西),所以我可以对数据进行切片和切块以找出它有什么问题。 (例如,我早期的一个理论是负坐标可能有一些东西,所以我做了一些 mapmin() 来找到最负的 X 和 Y 值,然后再做一些 map 创建一个数组,其中所有点都偏移一个常数。)

现在,为了使用点数组制作路径,我对 NSBezierPath 进行了扩展:

extension NSBezierPath {
    convenience init(linesBetween points: [CGPoint], stride: Int = 1)  {
        precondition(points.count > 1)
        self.init()
        move(to: points.first! )
        for i in Swift.stride(from: 1, to: points.count, by: stride) {
            line(to: points[i])
        }
    }
}

有了这个,我不仅可以轻松地从整个点阵列创建路径,还可以...

  • 跳过原始数组部分的路径(使用 stride 参数)

    let path5 = NSBezierPath(linesBetween: points, stride: 5)
    let path10 = NSBezierPath(linesBetween: points, stride: 10)
    

    (这对于更快地生成 playground 预览也很方便。)

  • 使用原始数组的某些块或切片的路径

    let zigzag = NSBezierPath(linesBetween: Array(points.prefix(to:10)) + [points.last!])
    let lopOffBothEnds = NSBezierPath(linesBetween: Array(points[1 ..< points.count < 1]))
    

或两者兼而有之...获奖作品(在上面的屏幕截图中)是:

let path = NSBezierPath(linesBetween: Array(points.suffix(from: 10)), stride: 5)

如果路径中有更多的点,您可以获得(稍微)更好的渲染效果,但更好的方法是用曲线而不是直线制作路径。对于额外的功劳,请尝试扩展上面的 NSBezierPath(linesBetween:) 初始化器以添加曲线,方法是将每个 n 点保留为路径的一部分,同时使用几个中间点作为控制手柄。 (这不是通用的自动跟踪算法,但对于这种情况可能已经足够了。)

这与 Rikster 的回答完全不同,但还有另一种方法可以防止此类问题。这是一种商业方式,可能有类似的免费软件应用程序,但这是我习惯使用的,它做得很好。

我说的 'this' 是什么?

通过名为 PaintCode 的应用程序将绘图转换为代码。这将允许您查看您的路径并确保它们有 none Rickster 指出的冲突是您的问题。

在这里查看:https://www.paintcodeapp.com/

此处的答案中列出了其他选项:How to import/parse SVG into UIBezierpaths, NSBezierpaths, CGPaths?