VoxelPanda/VoxelPanda/AAPLSceneViewController.m
/* |
Copyright (C) 2015 Apple Inc. All Rights Reserved. |
See LICENSE.txt for this sample’s licensing information |
Abstract: |
This view controller contains the Model IO code which is the focus of the sample, which is the loading of voxels. The voxels are displayed using SceneKit as an example graphics library. |
*/ |
@import SceneKit; |
@import ModelIO; |
@import SceneKit.ModelIO; |
#import "AAPLSceneViewController.h" |
@implementation AAPLSceneViewController |
{ |
SCNNode *_character; |
SCNNode *_voxels; |
BOOL _explodeUsingCubes; |
} |
#define SCALE_FACTOR 0.1 |
static float rnd() |
{ |
return 0.01 * SCALE_FACTOR * ((rand() / (float)RAND_MAX) - 0.5); |
} |
// Set up scene with character.scn asset and set view properties |
- (void) awakeFromNib |
{ |
#if TARGET_OS_IPHONE |
self.sceneView = (SCNView *)self.view; |
// Set up tap gesture recognizer |
UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTapGesture:)]; |
[self.view addGestureRecognizer:tapGesture]; |
#endif |
// Load scene from file path |
SCNScene *characterScene = [SCNScene sceneNamed:@"character.scnassets/character.scn"]; |
// For each static physics body in the scene, set its shape and remove the node's geometry so it doesn't appear in the scene. |
SCNNode *collisionNode = [characterScene.rootNode childNodeWithName:@"collision" recursively:YES]; |
[collisionNode enumerateChildNodesUsingBlock:^(SCNNode * __nonnull child, BOOL * __nonnull stop) |
{ |
child.physicsBody.physicsShape =[SCNPhysicsShape shapeWithGeometry:child.geometry options:nil]; |
child.geometry = nil; |
}]; |
// Set view properties and defaults |
_character = [characterScene.rootNode childNodeWithName:@"character" recursively:YES]; |
self.sceneView.scene = characterScene; |
self.sceneView.jitteringEnabled = YES; |
self.sceneView.allowsCameraControl = NO; |
} |
- (IBAction)voxelize:(id)sender |
{ |
// Create MDLAsset from scene |
SCNScene *tempScene = [SCNScene scene]; |
[tempScene.rootNode addChildNode:_character]; |
MDLAsset *asset = [MDLAsset assetWithSCNScene:tempScene]; |
// Create voxel grid from MDLAsset |
MDLVoxelArray *grid = [[MDLVoxelArray alloc] initWithAsset:asset divisions:25 interiorShells:0 exteriorShells:0 patchRadius:0.f]; |
NSData *voxelData = [grid voxelIndices]; // retrieve voxel data |
if (voxelData && voxelData.bytes) |
{ |
// Create voxel parent node and add to scene |
[_voxels removeFromParentNode]; |
_voxels = [SCNNode node]; |
[self.sceneView.scene.rootNode addChildNode:_voxels]; |
// Create the voxel node geometry |
SCNBox *particle = [SCNBox boxWithWidth:2.f * SCALE_FACTOR height:2.f * SCALE_FACTOR length:2.f * SCALE_FACTOR chamferRadius:0.f]; |
// Get the character's texture map and convert to a bitmap |
NSURL *url = _character.childNodes[0].geometry.firstMaterial.diffuse.contents; |
assert([url isKindOfClass:NSURL.class]); // this sample assumes that the `diffuse` material property is an URL to an image |
CGImageRef image; |
#if TARGET_OS_IPHONE |
image = [[UIImage imageWithContentsOfFile:[url path]] CGImage]; |
#else |
image = [[[NSImage alloc] initByReferencingURL:url] CGImageForProposedRect:nil context:nil hints:nil]; |
#endif |
CFDataRef pixelData = CGDataProviderCopyData(CGImageGetDataProvider(image)); |
UInt8 *buf = (UInt8 *)CFDataGetBytePtr(pixelData); |
size_t w = CGImageGetWidth(image); |
size_t h = CGImageGetHeight(image); |
size_t bpr = CGImageGetBytesPerRow(image); // this sample assumes 8 bits per component |
size_t bpp = CGImageGetBitsPerPixel(image) / 8; |
// Traverse the NSData voxel array and for each ijk index, create a voxel node positioned at its spatial location |
MDLVoxelIndex *voxels = (MDLVoxelIndex *)voxelData.bytes; |
size_t count = voxelData.length / sizeof(MDLVoxelIndex); |
for (int i = 0; i < count; ++i) |
{ |
vector_float3 position = [grid spatialLocationOfIndex:*voxels++]; |
#define OFFSET_FACTOR 0.9 |
// Determine color of the voxel by performing a hit test and then getting the texture coordinate at the point of intersection |
NSArray *results = [self.sceneView.scene.rootNode |
hitTestWithSegmentFromPoint:SCNVector3Make(position.x, position.y, position.z + 1.f) |
toPoint:SCNVector3Make(position.x * OFFSET_FACTOR , position.y * OFFSET_FACTOR, position.z - 5.f) |
options:@{SCNHitTestRootNodeKey : _character, SCNHitTestBackFaceCullingKey : @(NO)}]; |
#if TARGET_OS_IPHONE |
UIColor *color = [UIColor darkGrayColor]; // default voxel color |
#else |
NSColor *color = [NSColor darkGrayColor]; |
#endif |
if (results.count) |
{ |
SCNHitTestResult *result = results[0]; |
CGPoint tx = [result textureCoordinatesWithMappingChannel:0]; |
// Get the bitmap pixel color at the texture coordinate |
int x = tx.x * w; |
int y = tx.y * h; |
int pixel = bpr * round(y) + bpp * round(x); |
CGFloat r = (CGFloat)buf[pixel] / (CGFloat)255.0; // this sample code assumes that the first 3 components are R, G and B |
CGFloat g = (CGFloat)buf[pixel+1] / (CGFloat)255.0; |
CGFloat b = (CGFloat)buf[pixel+2] / (CGFloat)255.0; |
#if TARGET_OS_IPHONE |
color = [UIColor colorWithRed:r green:g blue:b alpha:1]; |
#else |
color = [NSColor colorWithCalibratedRed:r green:g blue:b alpha:1]; |
#endif |
} |
// Create the voxel node and set its properties |
SCNNode *voxelNode = [SCNNode nodeWithGeometry:[particle copy]]; |
voxelNode.position = SCNVector3Make(position.x + rnd(), position.y, position.z + rnd()); |
SCNMaterial *material = [SCNMaterial material]; |
material.diffuse.contents = color; |
material.selfIllumination.contents = @"character.scnassets/textures/max_ambiant.png"; |
voxelNode.geometry.firstMaterial = material; |
// Add voxel node to the scene |
[_voxels addChildNode:voxelNode]; |
} |
_explodeUsingCubes = true; |
CFRelease(pixelData); |
} |
} |
- (IBAction)dispalyVoxelsAsCubes:(id)sender |
{ |
if (!_explodeUsingCubes) |
{ |
SCNBox *cube = [SCNBox boxWithWidth:2.f * SCALE_FACTOR height:2.f * SCALE_FACTOR length:2.f * SCALE_FACTOR chamferRadius:0.f]; |
// For each voxel node, change its geometry to a cube |
[_voxels enumerateChildNodesUsingBlock:^(SCNNode * child, BOOL * stop) |
{ |
SCNMaterial *material = child.geometry.firstMaterial; |
child.geometry = [cube copy]; |
child.geometry.firstMaterial = material; |
}]; |
_explodeUsingCubes = true; |
} |
} |
- (IBAction)dispalyVoxelsAsSpheres:(id)sender |
{ |
if (_explodeUsingCubes) |
{ |
SCNSphere *sphere = [SCNSphere sphereWithRadius:1.f * SCALE_FACTOR]; |
// For each voxel node, change its geometry to a sphere |
[_voxels enumerateChildNodesUsingBlock:^(SCNNode * child, BOOL * stop) |
{ |
SCNMaterial *material = child.geometry.firstMaterial; |
child.geometry = [sphere copy]; |
child.geometry.firstMaterial = material; |
}]; |
_explodeUsingCubes = false; |
} |
} |
- (IBAction)explode:(id)sender |
{ |
// The shape of the physics body varies depending on the geometry of the voxel node |
SCNGeometry *particle; |
if (_explodeUsingCubes) |
{ |
particle = [SCNBox boxWithWidth:1.9 * SCALE_FACTOR height:1.9 * SCALE_FACTOR length:1.9 * SCALE_FACTOR chamferRadius:0.f]; |
} else |
{ |
particle = [SCNSphere sphereWithRadius:0.9 * SCALE_FACTOR]; |
} |
// For each voxel node, apply a physics force |
[_voxels enumerateChildNodesUsingBlock:^(SCNNode * child, BOOL * stop) |
{ |
child.physicsBody = [SCNPhysicsBody dynamicBody]; |
child.physicsBody.physicsShape = [SCNPhysicsShape shapeWithGeometry:particle options:nil]; |
[child.physicsBody applyForce:SCNVector3Make(rnd() * 1000.f, 3.f + 100.f * rnd(), rnd() * 1000.f) atPosition:SCNVector3Make(0.f, 0.f, 0.f) impulse:YES]; |
}]; |
} |
- (IBAction)reset:(id)sender |
{ |
[_voxels removeFromParentNode]; |
[self.sceneView.scene.rootNode addChildNode:_character]; |
} |
#if TARGET_OS_IPHONE |
- (void)handleTapGesture:(UITapGestureRecognizer *)gesture |
{ |
CGRect targetRectangle = CGRectMake(self.view.bounds.size.width * .5 - 50, self.view.bounds.size.height * .25, 100, 100); |
[[UIMenuController sharedMenuController] setTargetRect:targetRectangle inView:self.view]; |
// Create custom menu items |
UIMenuItem *voxelizeMenuItem = [[UIMenuItem alloc] initWithTitle:@"Voxelize" action:@selector(voxelize:)]; |
UIMenuItem *cubesMenuItem = [[UIMenuItem alloc] initWithTitle:@"Cubes" action:@selector(dispalyVoxelsAsCubes:)]; |
UIMenuItem *spheresMenuItem = [[UIMenuItem alloc] initWithTitle:@"Spheres" action:@selector(dispalyVoxelsAsSpheres:)]; |
UIMenuItem *explodeMenuItem = [[UIMenuItem alloc] initWithTitle:@"Explode" action:@selector(explode:)]; |
UIMenuItem *resetMenuItem = [[UIMenuItem alloc] initWithTitle:@"Reset" action:@selector(reset:)]; |
// Add menu items to shared menu controller |
[[UIMenuController sharedMenuController] setMenuItems:@[voxelizeMenuItem, cubesMenuItem, spheresMenuItem, explodeMenuItem, resetMenuItem]]; |
// Make menu controller visible |
[[UIMenuController sharedMenuController] setMenuVisible:YES animated:YES]; |
} |
- (BOOL)canBecomeFirstResponder { |
return YES; |
} |
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender |
{ |
if (action == @selector(voxelize:) || action == @selector(dispalyVoxelsAsCubes:) || action == @selector(dispalyVoxelsAsSpheres:) || |
action == @selector(explode:) || action == @selector(reset:)) |
{ |
return YES; |
} |
return NO; |
} |
#endif |
@end |
Copyright © 2015 Apple Inc. All Rights Reserved. Terms of Use | Privacy Policy | Updated: 2015-12-10