Create a Cube Sphere in SceneKit

前言

Cube Sphere 是一种奇特的球体,不同于 Geodesic Sphere 和一般 Sphere,更像是一个 Cube 通过某种变换得到的 Sphere。

Cube Sphere 有个好处,就是更适合来做过程生成的星球,因为每个面都可以用 QuadTree 无限细分(详见:https://acko.net/blog/making-worlds-4-the-devils-in-the-details/

过程

1.新建一个 SCNBox
2.对其修改顶点(vertices)
3.重新计算 Normal
4.刷新 SCNode 的 Geometry

1.新建 SCNBox

SCNBox *SCNBoxToSphereMapping = [SCNBox boxWithWidth:60.0f height:60.0f length:60.0f chamferRadius:0.0f];
    SCNBoxToSphereMapping.widthSegmentCount = 16;
    SCNBoxToSphereMapping.heightSegmentCount = 16;
    SCNBoxToSphereMapping.lengthSegmentCount = 16;
    
    SCNNode *PlanetNode = [SCNNode nodeWithGeometry:SCNBoxToSphereMapping];
    [PlanetSceneKitView.scene.rootNode addChildNode:PlanetNode];
    
    [SCNTransaction flush];
  • SegmentCount 是 2 的 n 次方 因为后面(当然不是这篇文章)要做 QuadTree
  • [SCNTransaction flush]; 是很关键的一步,(详见http://stackoverflow.com/questions/17760275/geometry-from-scenekit-primitives?lq=1 简单说来,就是不 flush 的话,获取的 geometry data 就是 SegmentCount = 1 的默认 SCNBox 数据,而默认的 SCNBox 顶点只有八个,不足以变换成球体)

2.对其修改顶点

// Get the vertex sources
    NSArray *vertexSources = [PlanetNode.geometry geometrySourcesForSemantic:SCNGeometrySourceSemanticVertex];
    
    // Get the first source
    SCNGeometrySource *vertexSource = vertexSources[0]; // TODO: Parse all the sources
    
    long stride = vertexSource.dataStride; // in bytes
    long offset = vertexSource.dataOffset; // in bytes
    
    long componentsPerVector = vertexSource.componentsPerVector;
    long bytesPerVector = componentsPerVector * vertexSource.bytesPerComponent;
    long vectorCount = (long)vertexSource.vectorCount;
    
    
    SCNVector3 vertices[vectorCount]; // A new array for vertices
    
    // for each vector, read the bytes
    for (long i=0; i<vectorCount; i++)
    {
        
        // Assuming that bytes per component is 4 (a float)
        // If it was 8 then it would be a double (aka CGFloat)
        
        //xyz 3 componet
        float vectorData[componentsPerVector];
        
        // The range of bytes for this vector
        NSRange byteRange = NSMakeRange(i*stride + offset, // Start at current stride + offset
                                        bytesPerVector);   // and read the lenght of one vector
        
        // Read into the vector data buffer
        [vertexSource.data getBytes:&vectorData range:byteRange];
        
        // At this point you can read the data from the float array
        
        //
        float x = vectorData[0] / SCNBoxToSphereMapping.width * 2.0f;
        float y = vectorData[1] / SCNBoxToSphereMapping.width * 2.0f;
        float z = vectorData[2] / SCNBoxToSphereMapping.width * 2.0f;
        
        float SphereX = x*sqrt(1-pow(y,2)/2.0f-pow(z,2)/2.0f + pow(y*z,2)/3.0f) * SCNBoxToSphereMapping.width / 2.0f;
        float SphereY = y*sqrt(1-pow(z,2)/2.0f-pow(x,2)/2.0f + pow(x*z,2)/3.0f) * SCNBoxToSphereMapping.width / 2.0f;

        float SphereZ = z*sqrt(1-pow(x,2)/2.0f-pow(y,2)/2.0f + pow(y*x,2)/3.0f) * SCNBoxToSphereMapping.width / 2.0f;

        
        // ... Maybe even save it as an SCNVector3 for later use ...
        vertices[i] = SCNVector3Make(SphereX, SphereY, SphereZ);
        
        // ... or just log it
        NSLog(@"x:%f, y:%f, z:%f", x, y, z);
        NSLog(@"SphereX:%f, SphereY:%f, SphereX:%f", SphereX, SphereY, SphereZ);
    }

3.重新计算 Normal

SCNGeometrySource *DeformedGeometrySource = [SCNGeometrySource geometrySourceWithVertices:vertices count:vectorCount];
    NSArray *SCNGeometrySourceArray = [NSArray arrayWithObject:DeformedGeometrySource];
    NSArray *DeformGeometryElement = [PlanetNode.geometry geometryElements];
    SCNGeometry *DeformedGeometry = [SCNGeometry geometryWithSources:SCNGeometrySourceArray elements:DeformGeometryElement];
    
    
    MDLMesh *DeformedGeometryUsingMDL = [MDLMesh meshWithSCNGeometry:DeformedGeometry];
    [DeformedGeometryUsingMDL addNormalsWithAttributeNamed:MDLVertexAttributeNormal creaseThreshold:1.0f];
  • 一个自定义 SCNGeometry 需要两样东西,SCNGeometrySource 和 SCNGeometryElements,SCNGeometryElements 在我的理解中就是用来描述顶点之间连接成三角形的顺序。当然实际肯定并非如此简单,只是便于理解我们只是修改了顶点的位置,但连接顺序并没有改变,因此 SCNBox 的 SCNGeometryElements 是可以继续用在新的 Sphere 上的
  • MDLMesh 是 Model I/O 的东西,用的时候和 SceneKit 一起记得导入以下头文件:
#import <SceneKit/SceneKit.h>
#import <ModelIO/ModelIO.h>
#import <SceneKit/ModelIO.h>

4.刷新 Geometry

DeformedGeometry = [SCNGeometry geometryWithMDLMesh:DeformedGeometryUsingMDL];
    PlanetNode.geometry = DeformedGeometry;

记得打开 Debug 的线框预览功能

PlanetSceneKitView.debugOptions = SCNDebugOptionShowWireframe;

效果如下: