在OS X上使用SceneKit处理成千上万个物体

8
我正在跟随这个教程:http://blog.bignerdranch.com/754-scenekit-in-mountain-lion/ 我对使用Scene Kit很感兴趣,但我的场景可能会有数千个球体。为了压力测试Scene Kit,我尝试了这个:
SCNSphere *sphere = [SCNSphere sphereWithRadius:0.5];
for (int i=0; i<10; i++) {
    for(int j=0; j<10; j++){
        for(int k=0; k<10; k++){
            SCNNode *myNode = [SCNNode nodeWithGeometry:sphere];
            myNode.position = SCNVector3Make(i,j,k);
            [root addChildNode:myNode];
        }
    }
}

这对于一千个球(10 ^ 3)来说很好,但对于一百万个球(100 ^ 3),它失败了(也许不足为奇)。我不介意不能使用一百万个球,但我想知道明智的上限是多少(5,000?15,000?)以及如何增加它。
我该怎么做才能缓解这种情况?例如,我尝试过sphere.segmentCount = 3,虽然它可以加速渲染,但在内存使用方面没有太大影响,我认为这是限制因素。
另外,似乎没有SCNPoint类。当球的数量太多时,我想切换到仅显示一个点,但我从SceneKit文档中看不出如何显示简单的点 - 我能看到的最简单的是三角形。
非常感谢任何帮助。
编辑:@toyos建议将SCNSphere对象合并为单个SCNGeometry对象(前提是它们不需要独立动画),但我找不到简单的方法来实现这一点。

使用[SCNGeometry geometryWithSources:(* NSArray)sources geometryWithElements:(* NSArray) elements]创建SCNGeometry,详见这里的文档说明,但我不清楚如何从我的球体创建一个SCNGeometry对象。

例如,对于单个球体,我可以使用sphere.geometryElementCount获取元素数量,然后使用[sphere geometryElementAtIndex:(NSInteger)elementIndex]来填充数组以获得元素,但我不确定如何获取“sources”(或它们是什么)。获取几何图形源的方法是[sphere geometrySourcesForSemantic:(NSString*) semantic],但这个语义字符串是什么?(它是“normals”还是“vertices”,还是其他什么?文档非常友好地说语义是“几何图形源的语义值。”,却没有说明语义的可能值是什么)

那只是针对单个球体的,这样做没有什么意义(因为SCNSphere只是SCNGeometry的一个子类),所以现在我需要添加多个球体。所以当将它们添加到我的SCNGeometry对象中时,我需要手动翻译球体的顶点吗?
我只是试图找出最明智的方法来完成这个任务。

我认为你正在超出SceneKit的预期用途。最好使用OpenGL绘制100万个球体,因为使用OpenGL不需要在内存中存储每个球体。使用OpenGL,您只需在渲染时将球体绘制到屏幕上即可。 - spudwaffle
1
我认为使用GL或SceneKit绘制1000k个球体,在这两种情况下只有一个球存在于内存中(可以看到“球”对象与1000k节点共享)。我认为问题是由于独立的绘制调用数量(每个球一个)。如果许多您的球不需要单独动画,您可以尝试将它们合并成单一几何体(使用SCNGeometry API)。 - toyos
有关使用SCNGeometry的任何指导?官方文档中似乎没有任何示例。我找到了这个网址http://www.cleoag.ru/2013/01/17/how-create-geometry-scenekit/,但是那会建议您需要手动定义顶点等。难道没有自动将我的球体“导入”到SCNGeometry对象的方法吗? - Matthew
1
你的球体是几何图形 - 所以不需要导入。例如,要获取顶点:[mySphere geometrySourcesForSemantic:SCNGeometrySourceSemanticVertex]。现在困难的部分是合并:您将不得不合并顶点数组(并对顶点应用平移),并合并元素数组(并偏移索引)。最后,使用合并的verge/normal/elements创建新的几何图形。 - toyos
@toyos,这真的非常有帮助,谢谢你。我会试一下的。 - Matthew
显示剩余2条评论
2个回答

8
语义字符串是SCNGeometrySourceSemanticVertex|Normal|Texcoord ... 对于多个球体,答案是肯定的,您必须在展平之前使用当前节点变换转换顶点/法线。以下是一个简化的示例(即仅支持合并“input”的子项,如果它们都具有相同的几何结构)。
- (SCNNode *) flattenNodeHierarchy:(SCNNode *) input
{
    SCNNode *result = [SCNNode node];

    NSUInteger nodeCount = [[input childNodes] count];
    if(nodeCount > 0){
    SCNNode *node = [[input childNodes] objectAtIndex:0];

        NSArray *vertexArray = [node.geometry geometrySourcesForSemantic:SCNGeometrySourceSemanticVertex];
        SCNGeometrySource *vertex = [vertexArray objectAtIndex:0];

        SCNGeometryElement *element = [node.geometry geometryElementAtIndex:0]; //todo: support multiple elements
        NSUInteger primitiveCount = element.primitiveCount;
        NSUInteger newPrimitiveCount = primitiveCount * nodeCount;
        size_t elementBufferLength = newPrimitiveCount * 3 * sizeof(int); //nTriangle x 3 vertex * size of int
        int* elementBuffer = (int*)malloc(elementBufferLength);

        /* simple case: here we consider that all the objects to flatten are the same
         In the regular case we should iterate on every geometry and accumulate the number of vertex/triangles etc...*/

        NSUInteger vertexCount = [vertex vectorCount];
        NSUInteger newVertexCount = vertexCount * nodeCount;

        SCNVector3 *newVertex = malloc(sizeof(SCNVector3) * newVertexCount);        
        SCNVector3 *newNormal = malloc(sizeof(SCNVector3) * newVertexCount); //assume same number of normal/vertex

        //fill
        NSUInteger vertexFillIndex = 0;
        NSUInteger primitiveFillIndex = 0;
        for(NSUInteger index=0; index< nodeCount; index++){
            NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

            node = [[input childNodes] objectAtIndex:index];

            NSArray *vertexArray = [node.geometry geometrySourcesForSemantic:SCNGeometrySourceSemanticVertex];
            NSArray *normalArray = [node.geometry geometrySourcesForSemantic:SCNGeometrySourceSemanticNormal];
            SCNGeometrySource *vertex = [vertexArray objectAtIndex:0];
            SCNGeometrySource *normals = [normalArray objectAtIndex:0];

            if([vertex bytesPerComponent] != sizeof(float)){
                NSLog(@"todo: support other byte per component");
                continue;
            }

            float *vertexBuffer = (float *)[[vertex data] bytes];
            float *normalBuffer = (float *)[[normals data] bytes];

            CATransform3D t = [node transform];
            GLKMatrix4 matrix = MyGLKMatrix4FromCATransform3D(t);

            //append source
            for(NSUInteger vIndex = 0; vIndex < vertexCount; vIndex++, vertexFillIndex++){
                GLKVector3 v = GLKVector3Make(vertexBuffer[vIndex * 3], vertexBuffer[vIndex * 3+1], vertexBuffer[vIndex * 3 + 2]);
                GLKVector3 n = GLKVector3Make(normalBuffer[vIndex * 3], normalBuffer[vIndex * 3+1], normalBuffer[vIndex * 3 + 2]);

                //transform
                v = GLKMatrix4MultiplyVector3WithTranslation(matrix, v);
                n = GLKMatrix4MultiplyVector3(matrix, n);

                newVertex[vertexFillIndex] = SCNVector3Make(v.x, v.y, v.z);
                newNormal[vertexFillIndex] = SCNVector3Make(n.x, n.y, n.z);
            }

            //append elements
            //here we assume that all elements are SCNGeometryPrimitiveTypeTriangles
            SCNGeometryElement *element = [node.geometry geometryElementAtIndex:0];
            const void *inputPrimitive = [element.data bytes];
            size_t bpi = element.bytesPerIndex;

            NSUInteger offset = index * vertexCount;

            for(NSUInteger pIndex = 0; pIndex < primitiveCount; pIndex++, primitiveFillIndex+=3){                
                elementBuffer[primitiveFillIndex] = offset + _getIndex(inputPrimitive, bpi, pIndex*3);
                elementBuffer[primitiveFillIndex+1] = offset + _getIndex(inputPrimitive, bpi, pIndex*3+1);
                elementBuffer[primitiveFillIndex+2] = offset + _getIndex(inputPrimitive, bpi, pIndex*3+2);
            }

            [pool drain];
        }

        NSArray *sources = @[[SCNGeometrySource geometrySourceWithVertices:newVertex count:newVertexCount],
                             [SCNGeometrySource geometrySourceWithNormals:newNormal count:newVertexCount]];

        NSData *newElementData = [NSMutableData dataWithBytesNoCopy:elementBuffer length:elementBufferLength freeWhenDone:YES];
        NSArray *elements = @[[SCNGeometryElement geometryElementWithData:newElementData
                                                            primitiveType:SCNGeometryPrimitiveTypeTriangles
                                                           primitiveCount:newPrimitiveCount bytesPerIndex:sizeof(int)]];

        result.geometry = [SCNGeometry geometryWithSources:sources elements:elements];

        //cleanup
        free(newVertex);
        free(newNormal);
    }

    return result;
}

//helpers:
GLKMatrix4 MyGLKMatrix4FromCATransform3D(CATransform3D transform) {
    GLKMatrix4 m = {{transform.m11, transform.m12, transform.m13, transform.m14,
        transform.m21, transform.m22, transform.m23, transform.m24,
        transform.m31, transform.m32, transform.m33, transform.m34,
        transform.m41, transform.m42, transform.m43, transform.m44}};
    return m;
}



GLKVector3 MySCNVector3ToGLKVector3(SCNVector3 vector) {
    GLKVector3 v = {{vector.x, vector.y, vector.z}};
    return v;
}

感谢花时间做这个。看起来是一个很好的解决方案!现在将进行测试 :) 并确保逐行理解它。 - Matthew

3
如何最好地做这取决于你想要实现什么。这些点(例如外太空场景的星场背景)是静态的,还是需要相互移动?它们实际上需要成为球体吗?需要多少细节?如果它们不需要独立移动,则合并它们成一个几何体是个好主意。在Mavericks(OS X 10.9)上,您不需要自己处理几何数据即可完成此操作 - 为每个节点创建一个节点,然后将其全部父级到单个节点(而不是场景的根节点),并调用flattenedClone以获取几何体组合的该节点的副本。如果它们不需要太多细节,则有一些提高性能的选项。一种方法是减少球体几何体的段数 - 在渲染时只需要几个像素宽度的球体不需要5000个三角形,这是默认段数48的宽度。另一种方法是进一步减少三角形数。星星(或其他东西)足够小,甚至需要成为球体吗?如果可以设置场景,使它们始终朝向相机,则SCNPlane可能更好 - 最低段数仅为两个三角形。它们甚至需要成为三角形吗?Scene Kit可以呈现点 - 没有SCNGeometry子类用于它们,因为独立定位和转换单个点通常没有用。但是,您可以使用顶点位置数组和SCNGeometryPrimitiveTypePoint几何元素类型创建自定义几何体。如果要自定义单个点的呈现,则可以将着色器(或着色器修饰符)附加到几何体。

感谢您的输入 - 我最近实际尝试了“flattenedClone”,但它只能在非常大量的对象上实际减少内存使用量,并且似乎并没有对fps产生太大影响,但也许是因为我需要先调用“flush”或者还有其他问题。我计划很快重新审视这个问题。我认为您建议的“segmentCount”和新的“levelOfDetail”选项可能会真正有所帮助... - Matthew
这是只渲染一个点的正确方法吗?SCNVector3 point = SCNVector3Make(0, 0, 0);SCNGeometrySource * src = [SCNGeometrySource geometrySourceWithVertices:&point count:1];SCNGeometryElement * element = [SCNGeometryElement geometryElementWithData:nil primitiveType:SCNGeometryPrimitiveTypePoint primitiveCount:1 bytesPerIndex:8];SCNGeometry * ge = [SCNGeometry geometryWithSources:@[src] elements:@[element]]; - Coldsteel48

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接