【问题标题】:SceneKit: how to identify and access faces from 3D model?SceneKit:如何从 3D 模型中识别和访问人脸?
【发布时间】:2018-07-03 21:10:47
【问题描述】:

在 Blender 中,您可以查看和访问 3D 模型的每个面,如下所示:https://poly.google.com/view/6mRHqTCZHxw

是否可以在 SceneKit 中做同样的事情,即访问模型的每个面?

This question is similar 并暗示这是不可能的,但不确认 SceneKit 是否允许您以编程方式访问模型的所有面。 (它侧重于识别被触摸的脸。)

两个问题:

1) 你能以编程方式访问每张脸吗?

2) 您能否过滤并仅访问可见的人脸(即忽略“内部”或被其他人脸遮挡的人脸)?

【问题讨论】:

    标签: ios 3d polygon scenekit mesh


    【解决方案1】:

    @Xartec 对您的第一个问题的回答的实现 #1,同样基于 Apple 文档,在 Swift 5.3 中:

    extension SCNGeometryElement {
        var faces: [[Int]] {
            func arrayFromData<Integer: BinaryInteger>(_ type: Integer.Type, startIndex: Int = 0, size: Int) -> [Int] {
                assert(self.bytesPerIndex == MemoryLayout<Integer>.size)
                return [Integer](unsafeUninitializedCapacity: size) { arrayBuffer, capacity in
                    self.data.copyBytes(to: arrayBuffer, from: startIndex..<startIndex + size * MemoryLayout<Integer>.size)
                    capacity = size
                }
                    .map { Int($0) }
            }
    
            func integersFromData(startIndex: Int = 0, size: Int = self.primitiveCount) -> [Int] {
                switch self.bytesPerIndex {
                case 1:
                    return arrayFromData(UInt8.self, startIndex: startIndex, size: size)
                case 2:
                    return arrayFromData(UInt16.self, startIndex: startIndex, size: size)
                case 4:
                    return arrayFromData(UInt32.self, startIndex: startIndex, size: size)
                case 8:
                    return arrayFromData(UInt64.self, startIndex: startIndex, size: size)
                default:
                    return []
                }
            }
    
            func vertices(primitiveSize: Int) -> [[Int]] {
                integersFromData(size: self.primitiveCount * primitiveSize)
                    .chunked(into: primitiveSize)
            }
    
            switch self.primitiveType {
            case .point:
                return vertices(primitiveSize: 1)
            case .line:
                return vertices(primitiveSize: 2)
            case .triangles:
                return vertices(primitiveSize: 3)
            case .triangleStrip:
                let vertices = integersFromData(size: self.primitiveCount + 2)
                return (0..<vertices.count - 2).map { index in
                    Array(vertices[(index..<(index + 3))])
                }
            case .polygon:
                let polygonSizes = integersFromData()
                let allPolygonsVertices = integersFromData(startIndex: polygonSizes.count * self.bytesPerIndex, size: polygonSizes.reduce(into: 0, +=))
                var current = 0
                return polygonSizes.map { count in
                    defer {
                        current += count
                    }
                    return Array(allPolygonsVertices[current..<current + count])
                }
            @unknown default:
                return []
            }
        }
    }
    

    生成的数组是一个面数组,每个面包含一个顶点索引列表。
    关于如何从SCNGeometrySource 提取顶点的答案可以在https://stackoverflow.com/a/66748865/3605958 找到,并且可以更新以获取颜色。

    您将需要这个实现上面使用的chunked(into:) 方法的扩展:

    
    extension Collection {
        func chunked(into size: Index.Stride) -> [[Element]] where Index: Strideable {
            precondition(size > 0, "Chunk size should be atleast 1")
            return stride(from: self.startIndex, to: self.endIndex, by: size).map {
                Array(self[$0..<Swift.min($0.advanced(by: size), self.endIndex)])
            }
        }
    }
    

    对于#2,我不相信有办法。

    【讨论】:

    • 你得到这个错误:'[Int]' 类型的值没有成员'chunked' 通过添加这个来修复:extension Array { func chunked(into size: Int) -> [[Element]] { return stride(from: 0, to: count, by: size).map { Array(self[$0 ..
    • @Olof_t 确实,我完全忘记了这一点。我已经用您刚刚发布的内容编辑了我的帖子,对任何类型的Collection 进行了一些改进,感谢您的评论!
    【解决方案2】:

    可以,但是SceneKit中没有内置方便的方法让你这样做,所以你必须自己构建。

    1. 是的,如果您定义什么是面并将其映射到模型中的顶点。例如,您可以按照相同的顺序将 SCNGeometry 的 SCNGeometrySources 读入您自己的人脸对象数组。使用 faceIndex 你可以得到你的面孔数组的索引。要更新它们,您必须根据您自己的 faces 数组数据,以编程方式构建基于 SCNGeometrySources 的 SCNGeometry。

    注意,faceIndex 返回渲染的三角形而不是四边形/多边形,因此您必须对其进行转换(如果所有四边形都非常可行)。

    我正在开发一个基于 SceneKit 的应用程序,它基本上是一个适用于 ipad 专业人士的迷你 Blender。它使用带有顶点、边和面的对象的半边数据结构。这允许访问这些元素,但实际上它允许访问映射到模型的半边数据结构,这构成了替换渲染的几何图形的基础。

    1. 不直接。如果您将几何图形映射到数据模型,当然可以在渲染之前计算它,但不幸的是,Scenekit 没有提供一种方便的方法来了解哪些面没有被渲染。

    总而言之,面只是存储在 SCNGeometrySources 中的顶点和索引的集合。如果您添加为什么要添加面以及您想要对其顶点执行的操作,则可能会更容易提供更好的答案。

    编辑:根据您的评论“例如,如果他们点击脸部,脸部应该变成蓝色。”

    正如我上面提到的,一个面只是顶点和索引的集合,一个面本身没有颜色,它是可以有颜色的顶点。一个 SCNNode 有一个 SCNGeometry ,它有几个 SCNGeometrySources 保存有关顶点的信息以及它们如何用于渲染面。所以你要做的是从 faceIndex 到 SCNGeometrySource 中的相应顶点索引。然后,您需要将后者读入向量数组,根据需要更新它们,然后根据您自己的向量数组创建一个 SCNGeometrySource。

    正如我提到的,faceIndex 仅提供了渲染内容的索引,不一定提供您提供的内容(SCNGeometrySource),因此这需要将模型映射到数据结构。

    如果您的模型将由所有三角形组成并且具有与共享相反的唯一顶点,并且不交错顶点数据,那么 faceIndex 0 将对应于顶点 0、1 和 2,而 faceIndex 1 将对应于顶点 3、4 , 和 SCNGeometrySource 中的 5。在四边形和其他多边形以及交错的顶点数据的情况下,它变得更加复杂。

    简而言之,在 SceneKit 中无法直接访问人脸实体,但可以通过编程方式修改 SCNGeometrySources(带有顶点位置、颜色、法线 uv 坐标)。

    编辑 2:基于进一步的 cmets: 原始类型告诉 Scenekit 模型是如何构建的,它实际上并不转换它。所以它仍然需要对模型进行三角剖分。但是,如果所有三角形,并且如果模型使用唯一顶点(与与相邻面共享顶点相反,如果必要,模型 io 提供了将顶点从共享顶点拆分为唯一顶点的功能)并且如果 SCNGeometrySource 中的所有顶点都被实际渲染(其中如果模型构造正确,通常是这种情况),那么是的。可以对多边形做同样的事情,见https://developer.apple.com/documentation/scenekit/scngeometryprimitivetype/scngeometryprimitivetypepolygon

    只有当它们显然不是三角形时,多边形 5、3、4、3 才会对应于面索引 0、1、2、3。但是,根据每个多边形的顶点数,您可以确定将为多边形渲染多少个三角形。在此基础上可以得到对应verts的索引。

    例如,第一个多边形对应于面索引 0、1 和 2(需要 3 个三角形来创建具有 5 个顶点的多边形),第二个多边形是面索引 3,第三个多边形是面索引 4 和 5。

    在实践中,这意味着循环遍历元素中的多边形并添加到 faceCounter 变量(每个顶点增加 1,超过 2),直到达到与 faceIndex 相同的值。虽然在我自己的数据结构上,我实际上自己做了同样的基本转换并且效果很好。

    EDIT3:在实际步骤中:

    将 SCNGeometryElement 转换为整数数组。

    将具有颜色语义的 SCNGeometrySource 转换为向量数组。可能没有具有颜色语义的 SCNGeometrySource,在这种情况下您必须创建它。

    如果使用多边形图元,则循环遍历您从 SCNGeometryElement 创建的数组的第一部分(最多为图元的数量,在本例中为多边形),并保留一个计数器,为每个顶点添加 1 多于2. 因此,如果多边形有 3 个顶点,则将计数器加 1,如果多边形有 4 个顶点,则加 2。每次增加计数器时,因此对于每个多边形,检查是否已达到 faceIndex。到达包含敲击面的多边形后,您可以使用上图所示的映射从 SCNGeometryElement 的第二部分获取相应的顶点索引。如果您添加第二个变量并在循环遍历它们时将每个多边形的顶点数递增,您已经知道存储在元素中的顶点索引的索引。

    如果所有多边形都是四边形,则转换更容易,并且 faceindex 0 和 1 对应于多边形 0,面索引 2 和 3 对应于多边形 1。

    从 SCNGeometryElement 获得顶点索引后,您可以修改从 SCNGeometrySource 创建的数组中这些索引处的顶点。然后重新创建并更新 SCNGeometry 的 SCNGeometrySource。

    最后但同样重要的是,除非您使用自定义着色器,否则您通过 SCNGeometrySource 提供的顶点颜色只有在分配的材质具有白色作为漫反射时才会正确显示(因此您可能必须将基础纹理也设为白色)。

    【讨论】:

    • 感谢您的回答! (也祝你在迷你搅拌机上好运,听起来棒极了!)看起来 faceIndex 有时也会返回边,而不仅仅是三角形?
    • faceIndex 返回 GPU 在对几何体进行三角剖分后渲染的三角形的索引(它总是对四边形和其他多边形执行此操作)。所以它只指渲染的内容(三角形)。它不适用于线原语。
    • 嗯,很奇怪,根据对宇航员模型的测试,它似乎还返回了边,而不仅仅是三角形,但可能我们的测试是错误的。最终,我们要做的是让用户自定义每张脸。关于如何使用场景包以编程方式执行此操作的任何建议?例如,如果他们点击脸部,脸部应该变成蓝色。
    • 最简单的方法是将对象分成单独的元素。为您希望能够单独修改的每个部分分配不同的材料,然后 SceneKit 将自动将模型转换为多个元素(每种材料一个)。例如,如果您在 Blender(或类似工具)中选择头部的面部并为其分配一种材质,然后为模型的其余部分分配另一种材质,SceneKit 将在导入时将其转换为两个单独的元素。
    • ...获取hitresult节点的材质并改变它。
    猜你喜欢
    • 2019-10-30
    • 1970-01-01
    • 1970-01-01
    • 2021-11-08
    • 2011-12-11
    • 2020-05-31
    • 2017-08-28
    • 2020-06-03
    • 1970-01-01
    相关资源
    最近更新 更多