2

I've got a row dimensional array of values that I want to visualize in 3D and I'm using scene kit under OS X for it. I've done it in a clumsy manner by using each column as a point on the X axis, each row as a point on the Z axis, and each value as a normalized point on the Y axis -- I place a sphere at the vector defined by each data point. It works but it doesn't look too good.

I've also done this by building a mesh of lines based on @Matthew's function in Drawing a line between two points using SceneKit (the answer he posted, not the original question). For each point I use his function to draw two lines - one between my current point and the next point to the right and another between my current point and the next point towards the front (except when there is no additional column/row, of course).

Using the second method, my results look much better... however the performance is quite hideous! It takes quite a long time to complete the initial rendering, and if I use a trackpad/mouse to rotate or translate the scene, I might as well get a cup of coffee to wait until my system is usable again (and this is not much hyperbole). Using the sphere method, things render and update very quickly.

Any advice on how to improve the performance when using the lines method? (Note that I am not trying to add both lines and spheres at the same time.) Code-wise, the only difference between approach is which of the following methods gets called (and that for each point, addPixelAt... is called once, but addLineAt... is called twice for most points).

enter image description here

- (SCNNode *)addPixelAtRow:(CGFloat)row Column:(CGFloat)column size:(CGFloat)size color:(NSColor *)color
{
    CGFloat radius = 0.5;

    SCNSphere *ball = [SCNSphere sphereWithRadius:radius*1.5];
    SCNMaterial *material = [SCNMaterial material];
    [[material diffuse] setContents:color];
    [[material specular] setContents:color];
    [ball setMaterials:@[material]];

    SCNNode *ballNode = [SCNNode nodeWithGeometry:ball];
    [ballNode setPosition:SCNVector3Make(column, size, row)];

    [_baseNode addChildNode:ballNode];
    return ballNode;
}

enter image description here

- (SCNNode *)addLineFromRow:(CGFloat)row1  Column:(CGFloat)column1  size:(CGFloat)size1
                       toRow2:(CGFloat)row2 Column2:(CGFloat)column2 size2:(CGFloat)size2 color:(NSColor *)color
{
    SCNVector3 positions[] = {
        SCNVector3Make(column1, size1, row1),
        SCNVector3Make(column2, size2, row2)
    };

    int indices[] = {0, 1};

    SCNGeometrySource *vertexSource = [SCNGeometrySource geometrySourceWithVertices:positions count:2];

    NSData *indexData = [NSData dataWithBytes:indices length:sizeof(indices)];

    SCNGeometryElement *element = [SCNGeometryElement geometryElementWithData:indexData
                                                                primitiveType:SCNGeometryPrimitiveTypeLine
                                                               primitiveCount:1
                                                                bytesPerIndex:sizeof(int)];

    SCNGeometry *line = [SCNGeometry geometryWithSources:@[vertexSource] elements:@[element]];
    SCNMaterial *material = [SCNMaterial material];
    [[material diffuse] setContents:color];
    [[material specular] setContents:color];
    [line setMaterials:@[material]];

    SCNNode *lineNode = [SCNNode nodeWithGeometry:line];

    [_baseNode addChildNode:lineNode];
    return lineNode;
}
Community
  • 1
  • 1
mah
  • 39,056
  • 9
  • 76
  • 93
  • We would need to know more about the performance problems and the measurements that you have? Is the problem too many draw calls? Too many triangles? Is the CPU doing too much work? etc. – David Rönnqvist Oct 28 '14 at 18:54
  • Also, are you using the lines individually or are they just making up a mesh? (A screenshot could be useful to know what you are doing but it's not related to the performance problems) – David Rönnqvist Oct 28 '14 at 18:56
  • @DavidRönnqvist I'm not positive of the answers here, but I can say it's not only my application that suffers when this happens, it's the full system. What I can say is that even when using the sphere method, it's approximately a second before I can see the scene after my last node gets created. – mah Oct 28 '14 at 18:57
  • My lines are individually created; I have 10,000 data points, so 20k lines. – mah Oct 28 '14 at 18:57
  • If you turn on `showsStatistics` on the view, what does that tell you? – David Rönnqvist Oct 28 '14 at 18:58
  • With statistics on, with the spheres I see (diamond)10.2k (triangle)11.3M (dot)33.8M. Changing to lines, The values become 20.1k, 20.1k, 40.3k. I've embedded cropped pictures with these in my question. – mah Oct 28 '14 at 19:09
  • I just found the statistics expansion button. The sphere scene shows 39~40ms with a pink (Rendering) circle. With lines, the value is about 6.5ms, also for rendering. – mah Oct 28 '14 at 19:16

1 Answers1

5

From the data that you've shown in your question I would say that your main problem is the number of draw calls. Your's is in the tens of thousands, which is way too much. It should probably be a lot closer to ~100.

The reason why you have so many draw calls is that you have so many distinct objects in your scene (each line). The better (but more advanced solution) would probably be to generate a single element for the entire mesh that consists of all the lines. If you want to achieve the same rendering with that mesh (with a color from cold to warm based on the height) then you could do that in a shader modifier.

However, in your case I would start by flattening all the lines (since that would be the smallest code change and should still have a significant performance improvement in your case).

(Optimizing performance is always an iterative process. Once you fix one thing there will be another thing which is the most expensive operation. Without your code I can only say what would help with the current performance problem)

Create an empty node (without adding it to your scene) and generate all the lines, adding them to this node. Then create a flattened copy of that node by calling flattenedClone on the node that contains all the lines

SCNNode *nodeWithAllTheLines = [SCNNode node];
// create all the lines and add them to it...
SCNNode *flattenedNode = [nodeWithAllTheLines flattenedClone];
[_baseNode addChildNode:flattenedNode];

When you do this you should see a significant drop in the number of draw calls (the number after the diamond in the statistics) and hopefully a big increase in performance.

David Rönnqvist
  • 56,267
  • 18
  • 167
  • 205
  • Good general advice. The shader modifier is probably the best way to go. You don't even need to generate custom geometry for that — use an `SCNPlane` with the grid resolution you want. For the height map data, either pass an array of floats in as a shader uniform, or stuff it into a texture and sample that in the shader. – rickster Oct 28 '14 at 21:17
  • @rickster Yes, I forgot that you could modify the geometry in the fragment shader :) – David Rönnqvist Oct 28 '14 at 21:21
  • Also, even with the flattened node, you're doing a lot of work to construct 20k geometries that are essentially identical. If you go for that approach, you only need one `SCNGeometry` instance (with one source and element) that contains a line from, say, `{0,0,0}` to `{0,0,1}` in local space. Then use the `position`, `rotation`, and `scale` of each node to orient its line segment. – rickster Oct 28 '14 at 21:21
  • You can't modify vertex positions in a fragment shader, but you can sample from textures in the `SCNShaderModifierEntryPointGeometry` modifier. – rickster Oct 28 '14 at 21:23
  • I meant vertex shader :( – David Rönnqvist Oct 28 '14 at 21:29
  • @DavidRönnqvist can you give a hint as to when your book will be available? ;) – mah Oct 29 '14 at 13:14