• ios8之后苹果推出了一个3D模型渲染框架。SceneKit。但是国内针对这方面的教程并不是很多。前两天搞了一下也是一头雾水,终于把最基础的内容搞明白了之后,写下这篇随笔作为cnblogs的开篇,希望能一直写下去。  ...

          ios8之后苹果推出了一个3D模型渲染框架。SceneKit。但是国内针对这方面的教程并不是很多。前两天搞了一下也是一头雾水,终于把最基础的内容搞明白了之后,写下这篇随笔作为cnblogs的开篇,希望能一直写下去。

      SceneKit现在可以支持有限的几种模型,截止到我写这篇文章为止似乎只有.dae和.abc后一种模型我没有使用过。这篇文章只针对.dae模型写。

      首先如果是希望加载一个已有的,不需要程序在运行的时候动态添加的dae模型。那么我们可以直接新建一个game类型的工程。在选项中选择SceneKit,在程序中加载自带模型的那句话中将模型名称替换即可。本文主要讲一下如何导出dae模型,并在server端动态下载并显示。

      首先我们手中有一个.stl或者其他的模型文件,将模型文件转换成.dae文件我使用Blender。

      (1)在Blender中新建场景

      (2)在右上侧栏中将自动生成的Cube、Camera等3个物体删掉

      (3)导入我们已有的模型文件

      (4)调整我们的模型文件的方向、大小

      (5)在右上侧栏更改模型文件及子文件的名字为你要导出的dae文件的名字(这一步很重要!)

      (6)在左侧栏中Edit Options中点击Smooth

      (7)File->export->dae

      (8)在接下来的页面中,我们选择导出的位置和文件的名字,并且在左侧选项Texture中选择include material texture(同样重要!)

      接下来我们在桌面上新建一个文件夹,暂时起名为model,更改后缀为.scnassets,将我们生成好的模型文件拷贝进去。SceneKit对于动态添加文件夹写了两个脚本。不太清楚作用原理是什么,以后再研究吧。暂时知道怎么用就行。将copySceneKitAssets、scntool文件拷贝到model.scnassets所在的目录下,进入终端并cd到该目录下,运行

    1 ./copySceneKitAssets model.scnassets -o model-o.scnassets

    如果终端没有报错,并且生成了model-o.scnassets,则代表运行成功。

      接下来我们把生成的model-o.scnassets文件打包成zip文件,目的是为了能让iPhone客户端下载的时候文件更小。

      打包好了之后上传至服务器即可。

      两个可执行文件下载链接  http://download.csdn.net/detail/u013588047/8937773  

     

      接下来是重头戏,如何在程序中下载,解压,并显示呢。

      下载解压我使用了两个开源框架 AFNetworking 和 SSZipArchive ,朋友们可以自行查阅使用方法。

      一步一步来,先是下载,解压

     1 - (void)downloadZip {
     2     
     3     NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
     4     AFURLSessionManager *manager = [[AFURLSessionManager alloc] initWithSessionConfiguration:configuration];
     5     //这里我们用本地链接替代一下,可以使用任意url链接
     6     NSURL *URL = [NSURL URLWithString:@"file:///User/name/Desktop/model.scnassets.zip"];
     7     NSURLRequest *request = [NSURLRequest requestWithURL:URL];
     8     
     9     NSURLSessionDownloadTask *downloadTask = [manager downloadTaskWithRequest:request progress:nil destination:^NSURL *(NSURL *targetPath, NSURLResponse *response) {
    10         NSURL *documentsDirectoryURL = [[NSFileManager defaultManager] URLForDirectory:NSDocumentDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:NO error:nil];
    11         return [documentsDirectoryURL URLByAppendingPathComponent:[response suggestedFilename]];
    12     } completionHandler:^(NSURLResponse *response, NSURL *filePath, NSError *error) {
    13         NSLog(@"File downloaded to: %@", filePath);
    14         
    15         //对文件解压
    16         NSArray  *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    17         NSString *documentsDirectory = [paths objectAtIndex:0];
    18         NSString *inputPath = [documentsDirectory stringByAppendingPathComponent:@"/product-1-optimized.scnassets.zip"];
    19         
    20         NSError *zipError = nil;
    21         
    22         [SSZipArchive unzipFileAtPath:inputPath toDestination:documentsDirectory overwrite:YES password:nil error:&zipError];
    23         
    24         if( zipError ){
    25             NSLog(@"[GameVC] Something went wrong while unzipping: %@", zipError.debugDescription);
    26         }else {
    27             NSLog(@"[GameVC] Archive unzipped successfully");
    28             [self startScene];
    29         }
    30         
    31     }];
    32     [downloadTask resume];
    33 }

    而对于3d模型场景的创建,我们使用SCNSceneSource,代码如下

    1 NSURL *documentsDirectoryURL = [[NSFileManager defaultManager] URLForDirectory:NSDocumentDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:NO error:nil];
    2//这里的dae文件名字是我们导出时定义的文件名,下面一段代码中加载的SCNNode是我们之前在面板中改过的模型名
    3 documentsDirectoryURL = [documentsDirectoryURL URLByAppendingPathComponent:@"model.scnassets/cube.dae"]; 4 5 SCNSceneSource *sceneSource = [SCNSceneSource sceneSourceWithURL:documentsDirectoryURL options:nil];

    然后我们加载.dae文件中的模型,作为一个SCNNode,名字为我们在一开始改过的模型名

    1 SCNNode *theCube = [sceneSource entryWithIdentifier:@"Cube" withClass:[SCNNode class]];

    最后我们设置一下灯光等效果,其实是新建game文件中设置好了的,我们要做的是将SCNNode *theCube加载到Scene中

    // Create a new scene
    SCNScene *scene = [SCNScene scene];
        
    // create and add a camera to the scene
    SCNNode *cameraNode = [SCNNode node];
    cameraNode.camera = [SCNCamera camera];
    [scene.rootNode addChildNode:cameraNode];
        
    // place the camera
    cameraNode.position = SCNVector3Make(0, 0, 15);
        
    // create and add a light to the scene
    SCNNode *lightNode = [SCNNode node];
    lightNode.light = [SCNLight light];
    lightNode.light.type = SCNLightTypeOmni;
    lightNode.position = SCNVector3Make(0, 10, 10);
    [scene.rootNode addChildNode:lightNode];
        
    // create and add an ambient light to the scene
    SCNNode *ambientLightNode = [SCNNode node];
    ambientLightNode.light = [SCNLight light];
    ambientLightNode.light.type = SCNLightTypeAmbient;
    ambientLightNode.light.color = [UIColor darkGrayColor];
    [scene.rootNode addChildNode:ambientLightNode];
    
    // Add our cube to the scene
    [scene.rootNode addChildNode:theCube];
    
    // retrieve the SCNView
    SCNView *scnView = (SCNView *)self.view;
    
    // set the scene to the view
    scnView.scene = scene;
    
    // allows the user to manipulate the camera
    scnView.allowsCameraControl = YES;
    
    // show statistics such as fps and timing information
    scnView.showsStatistics = YES;
    
    // configure the view
    scnView.backgroundColor = [UIColor blackColor];

    这样我们就可以动态下载一个dae文件并显示了。

     

    注:原创文章,转载请注明原作者。

    后记:刚刚开始进行ios开发,文章中肯定有很多不正确与缺漏的地方,也有很多不求甚解的地方,希望大家多多指教。

     

    转载于:https://www.cnblogs.com/nkcsmk/p/4681128.html

    展开全文
  • 1、通过SCNGeometry或子类... 相关链接:iOS开发之SceneKit框架--SCNGeometry.h iOS开发之SceneKit框架--SCNParametricGeometry.h 2、创建多个场景SCNScene读取其子node SCNNode *newNo...

    1、通过SCNGeometry或子类SCNParametricGeometry创建

      相关链接:iOS开发之SceneKit框架--SCNGeometry.h  

                         iOS开发之SceneKit框架--SCNParametricGeometry.h

     

    2、创建多个场景SCNScene读取其子node

        SCNNode *newNode = [SCNNode node];
        newNode.position = SCNVector3Make(0, 5, 0);
        [self.sCNView.scene.rootNode addChildNode:newNode];
        SCNScene *sceneShip = [SCNScene sceneNamed:@"art.scnassets/ship.scn"];
        for (id node in sceneShip.rootNode.childNodes) {
            [newNode addChildNode:node];
        }

     

    转载于:https://www.cnblogs.com/xianfeng-zhang/p/9054939.html

    展开全文
  • 原文网址 http://www.zwqxin.com/archives/opengl/model-fbx-dae-format-import-animation.html

    原文网址 http://www.zwqxin.com/archives/opengl/model-fbx-dae-format-import-animation.html


    2011年的时候集中轰击了五款3D模型格式(obj、3ds、md2、md3、md5),那时候其实主要是从渲染方式和模型动画方式的进化角度来选择的,尤其是ID Tech的md系列,让这个世界的模型动画观念从最简单的“帧动画”到当前主流的“骨骼动画”进化。但是,即便是拥有骨骼动画的md5格式,也已经是将近10年前的诞生物了,那么这10年来,3d模型格式到底有没有发生了什么变化?——我们将从当今主流的格式略窥一二。

    作为开场前口水话,先来说一说实时渲染领域上的3d模型的些许历史罢。上世纪90年代初,由游戏产业带动的娱乐化三维实时渲染开始兴起,那时候的模型也只停留在非常简陋的阶段,甚至没必要专门的存储手段,更别谈什么模型动画了。但是随后当人们发现硬件和技术(值得一提的是,OpenGL也在此时应运而生)逐渐可以支持更复杂的静态模型数据的实时渲染时,存储问题也正式被重视。在没有一个统一标准的情况下,那些以往用于工业建模设计上的交换格式(简化版本),例如Autodesk 3DS Max下的.3ds和Wavefront软件下的.obj,就被选为最具代表性的两种主流静态模型格式了,情况甚至延续至今。

    随着90年代中后期视频游戏的爆发式发展,模型中的动画也自然变成需求的一部分,而引领动画技术快速发展的一个重要角色,就是前面说到的ID Software公司下的ID Tech系列引擎。包括至今被众多游戏制作者崇拜着的约翰·卡马克所策动的Doom系列和经典的C/S架构游戏Quake系列,ID Tech在世纪交换期可谓聚集式吸引了视频游戏领域的众眼球。在这众系列引擎中,也隐含着模型动画方式的进化:

    • md(或者称md1),推测应该是ID Tech 1(1996)所使用的模型格式或概念,是否包含动画信息尚不可知,但是它必然是后来此系列模型格式的基石;
    • md2,始于ID Tech 2(1997),真正地把动画中的“各帧模型”合并到一个模型中,利用帧数据(结合Morphing技术)还原动画,简单而数据量巨大;
    • md3,始于ID Tech 3 (1999),虽仍以帧数据还原动画,但同时引入了骨骼这一概念,把模型分为下身、上身、头和武器这几部分,通过骨骼节点连接,下身节点带动上身移动和旋转,以此类推——相当于加入了一条短骨骼,有效地降低数据冗余度,各部分的动画可以各种组合;
    • md4,草稿式的模型格式,它的存在印证着新千年初始期,ID Tech人员对更高效模型格式和渲染技术的探寻,但也只停留在概念阶段而被略去——但也有其他人实现了这一概念(mdr格式),也存在别的发展分支(mdl格式);
    • md5,始于ID Tech 4(2004),从03年完善到05年,是真正的支持骨骼动画、顶点蒙皮渲染的模型格式,随着著名的Doom3游戏进入大众的视线。

    随着新世代显卡的发展、各种商业非商业实时渲染引擎的出现和发展,骨骼动画被引入为主流的模型动画标准,至今仍为主流技术。这10年来,随着ID Tech的沉默(最新的ID Tech 5已不如往日辉煌)和各路人马的强大和异军突起,业界也有巨头期盼着制作出某种更通用的(被更多模型制作工具支持或转换,可被各方人员交换和重用的)模型格式标准,这其中就包括Autodesk、Microsoft和与我们OpenGL标准息息相关Khronos委员会。

    • fbx,[WIKI]源于Kaydara的FilmBox(后改为现称的MotionBuilder)软件(1996),后来被Autodesk收购,但是这个模型格式依然被一直发展,直至今天它还是“最”跟得上潮流的格式——鉴于Autodesk已经无法给3ds格式更好的动画信息支持了,现在fbx才是它的主打,也因为有个强大的爹,这个格式被广泛的建模软件和游戏引擎支持,也有引擎(像那个Xxity3D)把它作为主要支持的导入模型的;
    • x,这是Microsoft弄出来的模型格式,随着DirectX的SDK一起升级,后来也成为了骨骼动画的代表模型格式之一,这是微软为了把DX塑造成完整的自生态开发系统而采取的常见行为,虽然格式本身是开放的,但可见对Direct3D是无条件支持的(甚至有内置函数可以直接读取),这里我就不过多阐述了,因长期没更新,所以业界使用率没比md5更广泛得了去哪,常见于D3D的例子Demo;
    • dae,[WIKI]也称Collada模型,是非盈利性组织Khronos负责维护的开放性三维模型格式(现已成为 ISO标准之一),应该说跟OpenGL是同一级别的,作为一个标准,其目的自然是能被各方支持了,而毕竟Autodesk有自己的利益考虑,所以远没有像fbx那样完美的支持,但这不妨碍它现在还是能够与封闭的fbx分庭抗礼的主要模型格式。

    除了以上说的这些,其实还有很多模型格式,除了那些纯粹地用来保存建模软件的阶段结果的(.max、.blender之类)的,还有沿袭式地用于VR领域的(.vrml和.x3d之类),以及一些特定为某些游戏引擎所用的模型格式(像那Xxre的.Xxremesh和那Xxrlicht的Xxrmesh)。而本文主要还是聚焦于当前最流行的Fbx和Dae,切入正题。(在此希望不太懂顶点蒙皮原理的同学先自己学习下或者参考上面链接中的MD5模型导入的两文,不然看本文肯定觉得不知所云。)

    FBX

    如前所述,fbx是一种封闭的模型格式,这不仅说它通常作为二进制文件出现,而且是目前只能使用Autodesk提供的FBX SDK来操控这种文件。事实上,无论是3DS Max还是其SDK内置的Converter工具,都可以把其转换成ASCII的文本格式,虽然看上去有点JSON的样子,可事实上是全自给的“仅供观赏”的数据堆,也没有spec,通常也不会有人使用这种方式输出模型。好了,看来是必须借助其SDK了,所以首先要做的一件不太让人愉快的事情:加入这个SDK的库(lib&dll)。现在我用的是最新的fbxsdk-2013.3,下文仅就这版本兼容的模型而言。(注意,在预处理器选项中加入FBXSDK_NEW_API和FBXSDK_SHARED,如果你不想编译器抱怨一大堆的话。)

    这里首先说一下,对于Fbx(其实下文的Dae也是一样的),它保存的最大集合是一个Scene(场景),跟很多的游戏引擎所使用的概念是一致的,就是用场景节点树来组织成一个场景。以前的3ds[3DS文件结构的初步认识] 虽然也是树状地组织数据,但它是把不同类型数据堆抽象成节点,而Fbx/Dae则是纯粹地表述场景节点。所以对于后者们来说,即使把场景中多个物体/模型保存到同一个模型文件里,也是可以的,只不过是不同名字的节点而已,甚至把灯光、相机也可以抽象成节点而已。当然,对于我们编程者来说,一个模型文件仅仅对应一个模型是最自然的。所以接下来我只会谈及抽取模型和动画本身信息(事实上动画信息并非必须的——fbx和dae文件在没有动画或骨骼信息时,也就是静态模型了),不相关的部分则不涉及也不关心。

    因为有其自身的SDK帮我们分析fbx文件,所以对于fbx,只要去获取SDK的FbxImporter读取的结果来为我们所用就可以了——问题是要知道怎么获取我们需要的数据,你要让它直接告诉你每个网格数据的各个直接可用的顶点属性数组,那可为难了。我们还是必须把它分析出来的数据,转换成我们需要的数据的。所以还是先想清楚我们需要什么数据:1.动画信息(如果有的话);2.网格信息;3.关联前两者的骨骼信息(如果有的话)。在导入MD5模型([MD5模型的格式、导入与顶点蒙皮式骨骼动画I] [MD5模型的格式、导入与顶点蒙皮式骨骼动画II])时,其实也是一个寻找这些信息的过程(不过MD5可是把动画信息另外封成一个md5anim文件而已)。

    C++代码
    1. FbxScene *pScene = FbxScene::Create(pFbxManager, "ImporterScene");  
    2.   
    3. pSdkImporter->Import(pScene);  
    4.   
    5. PrepareAnimationInfo((Scene*)pScene);  
    6.   
    7. FbxNode *pRootNode = pScene->GetRootNode();  
    8.   
    9. if (pRootNode)  
    10. {  
    11.     for (int i = 0; i < pRootNode->GetChildCount(); ++i)  
    12.     {  
    13.         FbxNode *pNode = pRootNode->GetChild(i);  
    14.   
    15.         ProcessNode((Node*)pNode, szResDirectory);  
    16.     }  
    17. }  
    18.   
    19. SetupJointKeyFrameInfo();  

    在这里,我首先获取到这个场景(Scene),然后用PrepareAnimationInfo来预先查找场景中存在的动画信息(这里只简单查询动画的基本信息,通过Scene内的各FbxAnimStack下查各FbxAnimLayer,每个Anim Layer保存着一个动画,这里是把这些Layer的地址先存起来),然后获取场景的根节点,逐个处理其下属节点( ProcessNode函数里再轮询处理该节点的下属节点,递归调用 ProcessNode,完成整棵场景树的深度遍历),最后就是SetupJointKeyFrameInfo,获取具体的动画数据并把前两者的信息结合起来。

    场景节点除了网格对象(FbxNodeAttribute::eMesh)外还有其他多种类型,前面说过了,省略。对于每个Mesh网格对象,我们要得到它的几个顶点属性数组:位置、纹理坐标、影响的骨骼点(Joint)个数,以及这些骨骼点的索引(Index)和影响因子(Bias),另外对于法线切线这些,也可以顺便获取也可以自行计算。注意除了位置(和顶点属性索引)外其余属性并非每个mesh都有,注意判断了。其中,获取骨骼蒙皮信息(也就是这个Mesh的Skin)还是很值得注意的:

    C++代码
    1. FbxSkin *pSkinDeformer = (FbxSkin *)pMesh->GetDeformer(0, FbxDeformer::eSkin);  
    2.   
    3. for (int i = 0; i < pSkinDeformer->GetClusterCount(); ++i)  
    4. {  
    5.     FbxCluster *pCluster = pSkinDeformer->GetCluster(i);  
    6.   
    7.     int nInfluencedPointIndexCount = pCluster->GetControlPointIndicesCount();  
    8.     int *pInfluencedPointIndice = pCluster->GetControlPointIndices();  
    9.     double *pInfluencedPointWeights = pCluster->GetControlPointWeights();  
    10.   
    11.     if (pLinkingBoneNode && pInfluencedPointIndice && pInfluencedPointWeights)  
    12.     {  
    13.         t3DJoint *pJoint = new t3DJoint((pCluster->GetLink()->GetName());       
    14.   
    15.         pCluster->GetTransformMatrix(transMatrix);  
    16.         pCluster->GetTransformLinkMatrix(transLinkMatrix);  
    17.   
    18.         GetMatrixValue(transMatrix.Inverse(), &pJoint->mtPreFramePosed);  
    19.         GetMatrixValue(transLinkMatrix, &pJoint->mtBindPose);  
    20.  
    21.         transLinkMatrix = transLinkMatrix.Inverse() * transMatrix;  
    22.         GetMatrixValue(transLinkMatrix, &pJoint->mtPostFramePosed);  
    23.   
    24.         AddJoint(pJoint);  
    25.   
    26.         for (int iPtIndex = 0; iPtIndex < nInfluencedPointIndexCount; ++iPtIndex)  
    27.         {  
    28.             int nVertIndex = pInfluencedPointIndice[iPtIndex];  
    29.                       
    30.             tVertWeights.nAttachJointIndex = GetJointCount() - 1;  
    31.             tVertWeights.fWeightBias = (float)pInfluencedPointWeights[iPtIndex];  
    32.   
    33.             std::map<int, std::vector<t3DVertWeights>>::iterator pFind = VertJointInfo.find(nVertIndex);  
    34.   
    35.             if (VertJointInfo.end() != pFind)  
    36.                 pFind->second.push_back(tVertWeights);  
    37.             else  
    38.                 VertJointInfo.insert(std::make_pair(nVertIndex,   
    39.                     std::vector<t3DVertWeights>())).first->second.push_back(tVertWeights);  
    40.         }  
    41.     }  
    42. }  

    可见,获得这个网格的skin后,就可以去查询这个skin内的各个cluster了,每个cluster其实就对应一个joint【骨骼节点】(FBX SDK内习惯叫Bone【骨骼】,其实本质都是一样的,我们需要的是影响骨骼的对应数量的矩阵),接下来保存一个<int, std::vector<t3DVertWeights>>的map的过程就不多说了,这里的key值是给SDK索引顶点用的顶点index(注意不是顶点属性索引,而只是单纯顶点的索引),value就是对应的joint信息,对应多少个joint,vector里就存多少组信息。接下来最好像导入MD5时那样把数量规范化到4个以下,不然就不好传入vertex shader了。注意这里代码的重点:生成Joint的同时,也要获取对应的矩阵信息。

    FBX不像MD5那样还要自己计算bindpose下的顶点坐标,但是还是需要知道对于每个Joint,怎样把顶点从bindpose空间转换到模型空间。在MD5中[MD5模型的格式、导入与顶点蒙皮式骨骼动画II] ,这个转换只需乘以bindpose矩阵的逆矩阵就OK了,可是Fbx里可像是没那么简单哦(这还是我碰壁后去翻sdk的例子程序里的代码比对才知道的,那个惨):参见上面的代码,每个cluster(joint)可以通过GetTransformMatrix和GetTransformLinkMatrix获取两个矩阵(前者我也不太知道具体意义是啥,不妨自己望文生义一下,后者看来就是bindpose矩阵咯),不妨设为MTrans和Mbindpose。把顶点从bindpose空间转换到模型空间的“Joint影响矩阵”:

    Mjoint‘   =  MTrans -1   *  Mjoint  *   Mbindpose-1   *   MTrans   

    其中Mjoint是当前帧下的该骨骼Joint的变换矩阵(在之前也说过了,就是由该joint的位移旋转缩放信息构成,相当于该Joint的模型矩阵,注意这里不像MD5里可以省略缩放信息,FBX和DAE的动画信息里都是包含缩放信息的说),等式右边是传入shader的joint影响矩阵。对比MD5的公式,可以看到这里多了个“程咬金”:MTrans,居然还分左右地夹在两边,左边是逆矩阵,右边是原矩阵。这样,在每帧计算Joint矩阵时可别忘了它咯。在代码中,考虑到模块统一的问题,Mjoint的左右两边干脆被我封在mtPreFramePosed和mtPostFramePosed中了……

    接下来谈一下网格信息。Mesh类型的Node都能获得对应的FbxMesh,顶点属性大致是GetElement类函数获取(再根据GetMappingMode/GetReferenceMode来看怎样具体通过GetDirectArray/GetIndexArray获取数据,这点应该来说是好麻烦的,不过人家也是为了尽量压缩冗余数据)。还有一点就是fbx里保存的不一定是三角面片,也可能是四角面或多角面,为了为我们所用,须通过一些方法转换成三角面的索引顺序(如下的多重循环)或者直接通过SDK自带的FbxGeometryConverter来预先三角化(TriangulateInPlace)。

    C++代码
    1. for (int i = 0; i < pMesh->GetPolygonCount(); ++i)  
    2. {  
    3.     int nPolySize = pMesh->GetPolygonSize(i);  
    4.   
    5.     if (nPolySize < 3)  continue;  
    6.   
    7.     for (int nTriCount = 3; nTriCount <= nPolySize; ++nTriCount)  
    8.         for (int k = nTriCount - 1; k < nPolySize; ++k)  
    9.             for (int j = nTriCount - 3; j < nTriCount; ++j)  
    10.             {  
    11.                 if (j == nTriCount - 1)    j = k; 
    12.   
    13.                 int nVertexIndex = pMesh->GetPolygonVertex(i, j);  
    14.   
    15.                 vPosition = mtBindShape * pMesh->GetControlPointAt(nVertexIndex);  
    16.                 //.....

    这里更重要的是,得出的顶点(fbx内称control point)须进一步经过一个矩阵(mtBindShape)变换一下。这个叫做BindShape矩阵的矩阵,我的理解是,有时候模型制作者绑定骨骼节点参数时的基准并不是bindpose状态而是稍微对每个网格经过一个调整(缩放旋转移位)后再进行的,那么导出时就会给每个mesh生成这样个BindShape。看sdk自带例子中的这一步,应该就是获取此矩阵的方法了:

    C++代码
    1. const FbxVector4 lT = pMesh->GetNode()->GetGeometricTranslation(FbxNode::eSourcePivot);  
    2. const FbxVector4 lR = pMesh->GetNode()->GetGeometricRotation(FbxNode::eSourcePivot);  
    3. const FbxVector4 lS = pMesh->GetNode()->GetGeometricScaling(FbxNode::eSourcePivot);  
    4.   
    5. GetMatrixValue(FbxAMatrix(lT, lR, lS), &mtBindShape);//生成矩阵  

    在获得网格顶点信息、顶点骨骼信息、纹理信息之后,对于这个网格,还需要判断它是直接由骨骼驱动,还是通过Attach的方式(例如武器)绑在其他节点或骨骼上,这个对于模型正确性来说还是比较重要的。而最后,根据动画信息(之前获得的AnimLayer)和骨骼,通过GetCurve-KeyGetCount来获取关键帧的时间集,一一去计算出骨骼节点在每个关键帧时间点的变换矩阵(EvaluateGlobalTransform),即Joint矩阵。fbx文件看上去内部似乎真的存储了一个一个属性曲线(Curve)一样,非得弄这种类似采样的方法去获取动画过程中的各属性值,但相信其实存储的也就关键点和值,比起构造Curve-Sampling的方式,直接能够取得关键帧的各信息肯定效率更高,但SDK内没找到类似接口——结果是,模型导入的大部分耗时都花在EvaluateGlobalTransform这类函数上了。

    http://www.zwqxin.com

    DAE

     dae是纯文本的模型格式,其本质就是一个单纯的xml文件。相比fbx,对dae格式模型的载入我们拥有非常高的自由控制,但是我们也必须承担读取和分析数据信息这一工作——这也是最复杂的地方。

    首先是xml的开源读取工具,可以选择的范围很广,我也只按己习惯选用rapidxml,比起fbx那巨大的sdk开发包,rapidxml的这两三个头文件就能精悍地协助整个载入工作,是为dae最大的优点。其次是格式的文档,khronos的官方网站上就有详尽的spec和Reference Card,对整个dae格式的了解也就可以直接从这里入手。目前dae的最新版本是1.5(事实上1.4也差不多而已),但这也是08年的事情了,那时候我都还没跳坑吧,可见它不如fbx那样一直被注入精力去完善发展,而是到达一个安稳的阶段暂时停顿(目前的发展方向好像是),但是基本上我们需要的东西它已经完全包含了,跟fbx相比,也许就是文件体积的大之外,还有一点就是对动画的支持稍逊点点。对比fbx,资源较难找一点,也没见过含多套动画在内的模型,这些也就是上面提及的原因导致的吧。

    基本上,dae文件内一开始就把数据分成了好几大块。对我们来说最为有用的是VisualScenes(包含场景骨骼节点树)、Nodes(与VisualScenes类似,两者或互为补充)、Geometries(网格数据)、Materials/Effects/Images(材质相关信息)、Controllers(骨骼信息数据)、Animations(动画数据)、AnimationClips(全局的动画信息),其中静态模型一般也就包括VisualScenes和Geometries。我选择的处理顺序是VisualScenes/Nodes -> Geometries(含Controllers、Materials等) -> Animations(含AnimationClips)。也就是说,先把场景节点树整理出来(同时也找出所有的骨骼节点),然后处理网格骨骼信息,最后是动画。

    dae的数据读取其实就是一项苦逼工作,但是其实一开始推进后进展会比较快,网格、材质纹理、骨骼信息的数据很快就出来了,但是也很快就跌入痛苦的深谷,尤其是到达动画信息读取的阶段,会发觉很难把前面这些数据联结起来,渲染的结果出现问题,修改读取方式,好了正常了,再读取另一个模型,发现出现别的问题,试再另一个模型,发现新的问题……那时候真心奔溃。究其原因,就是dae模型数据组织的自由度实在太高——类似的数据信息,有多种存储方式——对于不同的建模工具或转换工具,只要符合spec,数据组织的方式就可以随它所欲地存储。这样导致的结果就是,对于我们现在的读取工作,必须不断结合针对不同测试用例有效的不同读取方式,形成更泛化的方式,使得读取类满足所有测试用例。是的,测试用例,即使现在我也只能说我的这个读取类仅对目前我用这堆测试用例有效,不排除某天对偶尔找来的一个dae模型失效——渲染结果不正确。为了适应不同的建模工具和转换工具,为了作为标准被大伙认同和采用,开源而较为弱势的Collada很明显变得被动,变得妥协——如同OpenGL的某个困境般。

    Dae模型的Joint矩阵计算并没有fbx那样存在一个"程咬金"MTrans的影响,甚至会直接给出各骨骼joint的bindpose逆矩阵(mtPostFramePosed),直接计算即可:Mjoint‘   =    Mjoint  *   Mbindpose-1      ,但是网格顶点还是同样可能需要经过一个bindshape矩阵(Controllers中的各网格的bind_shape_matrix字段)的变换。

     读取骨骼节点或场景节点的时候,我们主要获取的是它的变换矩阵,但是在dae这里,这个信息并不是那么直观的。还记得OpenGL3.x之前,顶点数据的模视矩阵变换,都习惯在CPU端用函数(glTranslate/glRotate/glScale)来控制的,在一个或多个矩阵栈内按合适顺序调用这些函数,以完成模型矩阵的构建。Dae格式规范中也继承了这种方式(Collada当前最新版本出来时还是GL2.x年代嘛),所以对一个场景节点,会看到如下的xml子句:

    XML/HTML代码
    1. <node id="Scene_Root1_CENTER_G_3">  
    2.     <translate sid="translation">0.237243 -0.051032 -0.410632</translate>  
    3.     <rotate sid="rotation_z">0.000000 0.000000 1.000000 7.869819</rotate>  
    4.     <rotate sid="rotation_y">0.000000 1.000000 0.000000 -170.329865</rotate>  
    5.     <rotate sid="rotation_x">1.000000 0.000000 0.000000 -1.480750</rotate>  
    6.     <scale sid="scale">1.000000 1.000000 1.000000</scale>  
    7.    <!-->//....<-->
    8. </node>

    等价的OpenGL2.x代码:

    C++代码
    1. glPushMatrix()  
    2.  glTranslatef(0.237243-0.051032,-0.410632 );  
    3.  glRoatef(7.869819, 0, 0, 1);  
    4.  glRoatef(-170.329865, 0, 1, 0);  
    5.  glRoatef(-1.480750, 1, 0, 0);  
    6.  glScalef(1, 1, 1);  
    7.  DrawNodeStuff("Scene_Root1_CENTER_G_3");  
    8. //...  
    9. glPopMatrix();  

    再复杂的变换,也可以这样的方式表达出来:

    XML/HTML代码
    1. <node id="l_knee" name="l_knee" sid="l_knee" type="JOINT">  
    2.     <translate sid="translate">1.833014 -0.024761 -0.002519</translate>  
    3.     <rotate sid="jointOrientZ">0 0 1.000000 2.742172</rotate>  
    4.     <rotate sid="jointOrientY">0 1.000000 0 -8.695618</rotate>  
    5.     <rotate sid="jointOrientX">1.000000 0 0 -120.102058</rotate>  
    6.     <rotate sid="rotateZ">0 0 1.000000 0</rotate>  
    7.     <rotate sid="rotateAxisX">1.000000 0 0 -167.476486</rotate>  
    8.     <!-->//....<-->
    9. </node>

    如果是以以前的OpenGL为载体,这样的格式也方便。可是现在对我们有用的是它们的合成结果(模型矩阵),是不是直接自行构建一下就可以了呢?如果没有动画的话那还好,如果存在一个动画,要求把上述sid的值为jointOrientX的角度值由0度到50度呢?为了适应dae的动画机制,我们必须预先把每个node的各行变换储存起来:

    C++代码
    1. enum VertInfoType  
    2. {  
    3.     VIT_Translate = 0,  
    4.     VIT_Rotate,  
    5.     VIT_Scale,  
    6.     VIT_Count,  
    7. };  
    8.   
    9. struct t3DNodeVertInfo  
    10. {  
    11.     t3DNodeVertInfo() : nComponent(-1){}  
    12.     t3DNodeVertInfo(VertInfoType nInfoType) : nType(nInfoType), nComponent(-1){}  
    13.     VertInfoType nType;  
    14.     ZWVector3    vInfo;  
    15.     int          nComponent;  
    16.     std::string  strIdentifier;  
    17. };  
    18.   
    19. struct t3DNodeVertInfoSet  
    20. {  
    21.     std::vector<t3DNodeVertInfo> DataVec;  
    22. };  
    23.   
    24. struct t3DNodeInfo  
    25. {  
    26.     t3DNodeInfo() : pParentInfo(NULL){}  
    27.     t3DNodeInfo       *pParentInfo;  
    28.     std::string        strName;  
    29.     std::string        strSidName;  
    30.     t3DNodeVertInfoSet DefaultVertInfoSet;  
    31. };  

    假设用一个t3DNodeInfo结构体来描述一个场景/骨骼节点的信息,这里的DefaultVertInfoSet就是一个t3DNodeVertInfo数组,每个t3DNodeVertInfo表达了一行的变换数据。在构建动画时,根据动画关键帧中需要变化的数据行的标识(譬如上面的jointOrientX),替换出实际的数据,再把数据替换后的整个DefaultVertInfoSet转换成一系列矩阵的相乘结果——该节点在该关键帧的变换矩阵(通常我会再把矩阵分解出位移/旋转/缩放值,以便骨骼动画更新时能够准确的插值——虽然直接插值一个矩阵的渲染结果应该也不会看得出什么大问题):

    C++代码
    1. ZWQuaternion qRotation;  
    2. ZWMatrix16 mtTransform;  
    3. ZWMatrix16 mtResTransform;  
    4.   
    5. for (std::vector<t3DNodeVertInfo>::reverse_iterator p = VertInfoSet.DataVec.rbegin(); p != VertInfoSet.DataVec.rend(); ++p)  
    6. {  
    7.     mtTransform.LoadIdentity();  
    8.   
    9.     switch (p->nType)  
    10.     {  
    11.     case VIT_Translate:  
    12.         mtTransform.SetTranslationPart(p->vInfo);  
    13.         break;  
    14.     case VIT_Rotate:  
    15.         qRotation.setIdentity();  
    16.         if (0 == p->nComponent)  
    17.             qRotation.setValueFromPitch(DegreeToRadian(p->vInfo.x));  
    18.         else if (1 == p->nComponent)  
    19.             qRotation.setValueFromYaw(DegreeToRadian(p->vInfo.y)); 
    20.         else if (2 == p->nComponent)  
    21.             qRotation.setValueFromRoll(DegreeToRadian(p->vInfo.z));  
    22.         else if (ZWMatrix16::ITEM_COUNT == p->nComponent)  
    23.             qRotation.setValueFromEulerAngles(DegreeToRadian(p->vInfo.y,  p->vInfo.x,  p->vInfo.z));  
    24.         qRotation.GenerateMatrix3X3(&mtTransform);  
    25.         break;  
    26.     case VIT_Scale:  
    27.         mtTransform.MultiplyScaling(p->vInfo);  
    28.         break;  
    29.     default:  
    30.         break;  
    31.     }         
    32.     mtResTransform = mtTransform * mtResTransform;  
    33. }  
    34.   
    35. RetrieveVertInfo(mtResTransform, ResTransform.vInfo[VIT_Translate], ResTransform.vInfo[VIT_Rotate], ResTransform.vInfo[VIT_Scale]);  

    变换的类型除了translate/rotate/scale,还有比较常见的就是matrix(直接提供变换矩阵),为了一致性,此时我也会把这个矩阵先分解成一组translate/rotate/scale存入DefaultVertInfoSet。动画信息的构建过程可以理解为把关键帧中的值对应替换default值,当然了,实际操作起来感觉还是会变得很复杂的——如前所述,通用性,要考虑的状况太多。最后洋洋洒洒的3000行数据读取分析代码便是佐证了。

    http://www.zwqxin.com

    模型包围盒(BoundingBox)

    对一个渲染系统里的渲染物件(Renderer)来说,包围盒是很重要的。包围盒的一个重要作用是提供视锥剔除(Frustum-View-Culling)的依据(计算物件的最终包围盒是否在视锥体内以判断是否提交到GPU作渲染),另外也可以做简单的碰撞检测之类。最简单的包围盒是AABB(Axis-Align-Bounding-Box),也就是无论渲染物件怎样旋转,其包围盒各边始终平行于世界坐标系的xyz轴,它只需要盒子的最小最大两个点坐标就可以描述。包围盒跟物件是同体的,所以只要得到物件在局部(本地)坐标系下的包围盒,乘上物件的模型矩阵,再重获变换后的立方体的最大最小点,就构建出最终包围盒了。

    对于静态的模型,它的局部包围盒很容易构建——传入VBO的顶点位置坐标,取其最大最小值即可;帧动画的模型,也就是取各帧的包围盒插值即可;问题是骨骼动画的模型——一般来说,模型各顶点的本地坐标值是在shader里计算出来的(顶点蒙皮,参见[MD5模型的格式、导入与顶点蒙皮式骨骼动画II] ),所以无法在应用端获取。难道需要预先进行一次CPU端的蒙皮演练?

    在MD5模型中,存在BoundingBox的字段,可以直接取得各帧的模型包围盒。但是无论fbx还是dae都没有这种强制性机制,当然建模人员可以以某种方式预先确定好包围盒,存入这两种模型格式的“用户自定义”字段中,但是对于我们这种比较注重通用性的模型导入模块中,这种方式是不可以预先依赖的——更需要一种通用的方式,计算骨骼动画模型的包围盒。

    实时地计算骨骼模型的包围盒,必然从其“骨骼”入手,因为本身骨骼的位置数据是每一帧都须计算的,可以直接取得。比较容易想到的是单纯计算骨骼的包围盒,再稍微膨胀一下——但是膨胀多少是很难确定的,并不保险。最好的方法,是先在导入阶段计算出bindpose空间下的各个网格对应影响该网格的各骨骼的包围盒——对每个骨骼Joint,计算其影响值大于0的网格顶点集(在bindpose空间下,注意是乘了bindshape矩阵的结果)的包围盒——Joint的bindpose包围盒。跟顶点集一样,运行时先后乘上该joint的bindpose逆矩阵和joint矩阵,得到局部坐标系下的joint包围盒——网格对象的包围盒直接由影响它的骨骼joint的包围盒合成(Merge):

    C++代码
    1. ZWBoundingBox ZWModelBase::t3DObject::GetRetrictingBoundingBox()  
    2. {  
    3.     if (bHasSkinning)  
    4.     {  
    5.         ZWBoundingBox boxMerged;  
    6.   
    7.         for (ZWModelJointList::iterator q = InfluencedJoints.begin(); q != InfluencedJoints.end(); ++q)  
    8.         {     
    9.             boxMerged.Merge((*q)->boxBindPose.WithTransform((*q)->mtPreFramePosed * (*q)->mtCurrent * (*q)->mtPostFramePosed));   
    10.         }  
    11.   
    12.         return boxMerged;  
    13.     }  
    14.     else  
    15.     {  
    16.         return ZWRenderer::GetBoundingBox();  
    17.     }  
    18. }  

    好了,非常完美的包围盒(RestrictingBounding),时刻准确地包围着模型——但是,场景中有多个模型时,运行时的上述这个计算,造成帧率的巨大下降!毕竟是针对每个网格每个骨骼的计算,而现在的一个模型随便就可以上百根骨骼的总数,造成了这种捡了芝麻丢了西瓜的状况——我们需要boundingbox来做剔除操作,目的不就是为了降低帧率嘛!必须换一种思路——必须在动画开始前就确定好一个针对该动画的最大包围盒。

    考虑到设置或更改模型动画时能一次性完成最大包围盒(MaxBounding,准确地说,是动画过程中始终能包围住网格物件的最小包围盒)的计算:

    C++代码 (各Joint的最大包围盒)
    1. void SetAnimationTime(t3DAnimation *pAnimation, float fStartTimeSec, float fEndTimeSec)  
    2. {  
    3.     std::string strAnimName = pAnimation->GetName();  
    4.   
    5.     ZWMatrix16 mtKeyFrame;  
    6.   
    7.     for (std::map<std::string, t3DJoint *>::iterator p = m_JointMap.begin(); p != m_JointMap.end(); ++p)  
    8.     {  
    9.         t3DJoint *pTargetJoint = p->second;  
    10.   
    11.         t3DJointAnim *pTargetJointAnim = pTargetJoint->GetAnimInfo(strAnimName);  
    12.         int nStartFrame = GetFrame(fStartTimeSec);
    13.         int nEndFrame = GetFrame(fEndTimeSec);
    14.   
    15.         for (int j = nStartFrame; j <= nEndFrame; ++j)  
    16.         {    
    17.             mtKeyFrame.SetFromFrameData(pTargetJointAnim->pKeyFrameSet);  
    18.   
    19.             mtKeyFrame = pTargetJoint->mtPreFramePosed * mtKeyFrame * pTargetJoint->mtPostFramePosed;   
    20.   
    21.             pTargetJoint->boxMaxCurrent.Merge(pTargetJoint->boxBindPose.WithTransform(mtKeyFrame));  
    22.         }  
    23.     }  
    24. //....  
    25. }  
    C++代码 (预计算各网格对象MeshObject的最大包围盒)
    1. ZWBoundingBox boxMerged;  
    2. for (ZWModelJointList::iterator q = InfluencedJoints.begin(); InfluencedJoints.end(); ++q)  
    3. {     
    4.     boxMerged.Merge((*q)->boxMaxCurrent);     
    5. }  
    6. MeshObject[i]->ResetBoundingBox(boxMerged);  

    http://www.zwqxin.com

    至此完成7种代表性的模型格式的导入和渲染(3ds、obj、md2、md3、md5、fbx、dae),放张图纪念一下。除非突发的需要或未来主流的变化,大概本系列的文章也暂止于此了,希望对想涉及这方面了解的同学有些助益罢。

    http://www.zwqxin.com

    于此结束本文。


    展开全文
  • DAE,即Collada,这里指定版本1.4.1。collada是一个开放的标准,最初用于3D软件数据交换,由SCEA发起,现在则被许多著名厂家支持如Autodesk、XSI等。目前的3D工具,如3dsmax、maya、blender等均支持导出collada格式...

    DAE,即Collada,这里指定版本1.4.1。collada是一个开放的标准,最初用于3D软件数据交换,由SCEA发起,现在则被许多著名厂家支持如Autodesk、XSI等。目前的3D工具,如3dsmax、maya、blender等均支持导出collada格式文件,你需要做的是下载对应工具的导出插件,地址:Go

    DAE数据格式文件采用DOM方式结构,由于涉及3D方方面面的描述,是一个很复杂的系统。经过一段时间的思考与实践,我终于能够理解dae(collada 1.4.1)模型数据结构,并理解了骨骼动画原理与实现。对于骨骼动画的理解是我很多年的愿望,如今我自己动手写shader,实现骨骼动画渲染,这是多么美好的事情呢!下面主要分享dae数据文件结构,模型组织方式,骨骼动画数据结构,并提供完整的基本实现版本和复杂实现版本源代码。希望能帮助你理解dae文件和骨骼动画!顺便吐槽下,dae文件数据表有英文也有日文,为啥组织不能搞个中文版呢?kill you, laowai!

    dae采用xml格式存储,对比讲这种格式浪费了一定的存储空间,但是由于是khronos提出的标准3D格式交互文件,能够直观看到数据源。研究清楚了dae,对于其他3d格式,比如3ds、md2、md5等,理解起来自然很容易了。关于dae格式的解析wazim进行了详细的分析,可惜是english的,看的头大,并且他做了很多假设,这也是个大问题,实际模型都很复杂。

    1 整体框架结构

    先从整体看看dae格式文件结构。如上图,根节点下面有asset、libray_animations等节点,他们对应的功能如下:

    >  额外信息
    asset — 关于本dae数据文件的一些信息,包括作者、使用的工具,创建、修改日期,基本单位,使用的朝向上坐标轴等。

    > 几何模型和材质
    几何模型信息在library_geometries节点中存储,包括三角形顶点、索引,关联材质等信息。材质信息可以通过library_images、library_materials、library_effects节点联合查找到。

    >  场景和一个场景中模型组织结构
    通过scene节点找到对应单场景的根节点。通过library_visual_scenes找到该根节点下面的node节点信息。同时library_nodes提供更多的node结构细节。主要是树形结构,加嵌套。注意的是,骨骼框架结构也是在library_visual_scenes节点中存储的。

    > 骨骼蒙皮信息
    骨骼蒙皮信息存放在library_controllers节点中。所谓蒙皮实际是那些顶点被那些骨骼节点作用,权重是多少等。

    > 骨骼动画数据
    骨骼动画数据存放在library_animations中,如果有剪辑将被存放在 library_animation_clips中。

    2 模型和材质解析

    library_geometries节点下有一个geometry节点,这个节点就是一个模型的几何结构单元。想象一下,汽车有车身、轮子等,轮子的几何结构就是通过geometry节点描述的。实际中library_geometries可能有多个geometry节点,也就是有多个几何模型单元。如果搞懂了geometry节点,其他的geometry只需要迭代就可以了。geometry下面有三个source节点,一个vertices节点,还有triangles节点。source节点是标准的dae数据单元,下文你将看到很多这种数据单元。

    如上图,看technique_common节点下的accessor(存取器),告诉我们这个source的数据组成,本实例是3个float型的,stride表示步长,即3个float一个单元,共有count=333个数据单元。很明显,这里意思是一个顶点有x,y,z组成,共有333个顶点数据。其中的type属性可能是float、string等。

    读取geometry的时候,首先想到的是triangles部分,因为该部分描述了三角形信息,本例中triangles下面有三个input,这个是关键。semantic表示该input的意义,有VERTEX、NORMAL、TEXCOORD等类型,第一个input,表示类型是顶点,数据源是id=#demoman-mesh-vertices的source单元,偏移是0。注意这个id是使用url标示的,一般url属性前面的#都是要除掉的。第二个input,表示顶点法线,数据源是id=demoman-mesh-normals的source单元 ,偏移是1。第三个input,表示贴图uv坐标值,数据源是id=demoman-mesh-map-channel1的source单元 ,偏移是2。综合起来看,表示triangles节点下的p的数据表示为三个数据一组(几个input,然后加上偏移,就能知道几个一组),第一个表示顶点index,源是demoman-mesh-vertices。第二个表示顶点法线index,源是demoman-mesh-normals。第三个表示纹理贴图index,源是demoman-mesh-map-channel1。在解析的时候,将首先解析source数据单元,通过p节点的index很快能定位对应实际数据。注意triangles节点的属性material,表示该三角形组的贴图id,这里等于demoman_red。这个id,接下来我们分析。

    需要注意的是,对于复杂模型,存在一个geometry下有多个triangles,或者是多个lines,还有多边形(polygons),这要求设计对应的数据结构支持这种模型。如下图:

    上面提到贴图id,这个id是在library_materials中标示的。找到对应id的material节点,子节点instance_effect的url属性表示该material对应的effectId。effectId在library_effects节点中存放。如下示例,effectId=demoman_red-fx,找到instance_effect节点去看个究竟。

    library_effects一般有多个effect节点,分别描述对应的材质的一些属性,如下图:effect节点的子节点newparam之一描述了材质贴图路径。具体是init_from指明。而这个值可以在library_images找到对应id匹配的项,其init_from就是具体贴图路径了,本实例贴图路径是model/demoman_red.jpg。需要注意的是library_effects下effect下profile_COMMON下的technique节点,该节点描述了材质更多的信息,比喻漫反射、环境光、透明度等。是否双面(double_sided)存放在effect下面的extra子节点中。

    直至你应该比较清晰了模型的几何结构,和材质贴图方面的描述。如果要解析模型,还必须知道模型节点的组织结构。在解析代码中,我首先完成了几何模型和贴图的解析,建立geometry的id和实例的对应,方便下面的进一步处理,实际下面所说的节点Node只是层次结构描述,必须通过instance_geometry节点挂钩具体的几何模型单元。

    3 场景组织和模型节点组织

    这样想象一下,3d建模的时候,可能场景有多个物件,比喻一个场景有房子、人物。房子是一个模型,可能有窗户、门、墙等组成。而人由头、身子、手、脚等组成。scene节点相当于场景的所有物件的根节点。如下图,根节点id值为demo_rigged.max。拿着这个id去library_visual_scenes去找,整个物件的节点信息都可见了。

    现在问题是如何组织一个物件的结构?为了简化这个问题,换一个模型实例。如下图。物件的根节点是VisualSceneNode。其下面有一个node节点。一个node节点可以想象为物件的孩子,比如人的一部分手。显然手有自己的位置、旋转属性,于是matrix节点出现了,是一个16个数据的字符串,其实就是一个Matrix3D。有时候matrix不存在,就是默认单位矩阵吧!注意看子节点instance_geometry,这个是关键。表示该node引用这个几何模型,可以理解为这个手的具体几何模型信息就是链接几何id= MeshShape的几何模型。这个id就是library_geometries中出现的geometry节点的id。如下图:

    需要说明的是,这是模型节点结构中最简单的一种方式,复杂的情况是visual_scene下面有多个node,并且node又嵌套node。如下图:

    本实例中 visual_scene节点node的子节点使用instance_node引用了一个node,其属性url=WALL-E_mark_2。这个引用的node在library_nodes可以看做一根藤,通过这个藤把整个模型串起来。找到library_nodes下面node对应的id=WALL-E_mark_2的节点看其子节点,可以看到id=mesh27的node是直接引用一个几何模型url=mesh27-geometry,然后就完结了。对应节点id=wall_e_leg1的节点,引用了一个node节点id=wall_e_leg,这时候我们要去上一个层次去查找了。总的讲,dae的模型结构是嵌套的,使用引用的层次结构。

    分析到现在,对应解析静态模型,应该没有大问题了。通过递归library_visual_scenes和library_nodes(如果有)的Node层次结构,很容易建立整个模型的结构。记得将Node的id设置成对象的name属性,以便于调试。在我的实现代码中,使用YObject3DContainer对象对应一个Node,其子Node将被addChild进来。Node及子Node通过matrix的连乘实现位置、旋转、缩放属性的传递。

    4 骨骼动画解析


    如上图,在解析library_visual_scenes节点id=demoman-node的时候,遇到了该节点的instance_controller子节点。可以理解该节点记录了蒙皮控制器的链接信息。想象一下,人的组成:骨骼和外表组织。骨骼是有上下级关系,这个通过visual_scene下面的其他node节点可以得知,注意如果node节点的type属性=JOINT,表示该节点是一根骨骼,或者理解为一个joint(关节)节点。这里吐槽下,实际上所说的骨骼就是一个点,带变化矩阵的点。真正所说的骨骼可以理解为两个这样的点的连线。通过instance_controller节点属性url定位到library_controllers中,找到id=demoman-mesh-skin的controller节点,现在问题转换成解析蒙皮控制器。如下图:


    定位到library_controllers节点下controller,controller节点一般结构是:子节点skin,其source属性表示该蒙皮控制器作用的几何模型id。skin有bind_shape_matrix、source、joints、vertex_weights等节点。 逐个理解,bind_shape_matrix作用整个几何模型的矩阵,想象一下,把人的外表皮套到人的骨骼上,是不是需要左右偏移下外表皮,以适应骨骼框架(好恐怖的比方,我是如何想到的;()。joints下面有两个input,semantic分别为JOINT、INV_BIND_MATRIX。表示骨骼节点名称信息源的url、每根骨骼的转换矩阵的url。至于这几个source联系上面的讲解,其实就是些数据源,包括节点Joint名称源、对应节点Joint的绑定矩阵源、权重源。最后就是vertex_weights节点了,有两个input,一个Joint节点名称的source源id。另一个是作用权重source源id。总体想象一下,模型有多个顶点,并且一个顶点一般受到多个骨骼节点的作用。因此需要知道一个顶点被作用的骨骼id,和对应的权重。看看vertex_weights节点上的count属性值=333,刚好是顶点的个数。看下面例子,具体分析下:


    这个例子中,得知 vertex_weights节点的vcount节点第一个值是3。表示第一个顶点受到3个骨骼的作用。第一个骨骼的index是34,该骨骼权重的index值为1。同样,第二个骨骼的index是35,该骨骼权重的index值为2。有聊这些index,直接到对应的source源中查找,就能得到具体的是那根骨骼和权重数据。

    最后的问题就是骨骼动画数据了,真的最后一步了!

    总体讲,dae文件动画数据采用存储每一根骨骼一系列时间点上的变换矩阵。这个时间点相当于flash动画中的关键帧。两个关键帧采用插值的方式进行计算变换矩阵。如上图,libray_animations下面有多个animation子节点。一个animation节点下有source、sampler、channel节点。source就是数据源。先看下channel节点,其source属性标示了sampler源的id。target属性标示了动画的对象,具体就是那根骨骼,注意target值中的”/”后的内容,表示变换类型,具体的描述可以笼统认为是旋转、平移变化,一般缩放不被考虑。看到sampler节点,有三个input,通过semantic的标示得知:第一个input标示输入,其实就是一些列时间点。第二个input是输出,其实就是当前时间对应的变换矩阵的值。第三个input指示插值的方式,一般是LINEAR,即是线性插值。

    现在考虑下蒙皮骨骼动画的渲染。动画数据描述的是骨骼Joint在自己的坐标空间的变换矩阵系列,由于可以进行线性差值,通过公式:startMatrix*alpha+endMatrix*(1-alpha)很简单的计算插值后的矩阵,当然3D动画还有其他插值方式。通过骨骼间的层次关系,可以求得每一块骨骼世界坐标系中的变换矩阵。这里面还有一个BindPose变换矩阵。想象一下,人的一个站立动作,可以通过站立动作,人能自如切换到其他动作。比喻简单的举起右手,整个变换需要手臂绕人身子节点旋转,手臂的旋转使得手肘也旋转,手肘旋转,手掌等也随之变换。实际上就是几个关节的旋转,导致了整个手臂的旋转。BindPose相当于建立人的初始化姿势的变换矩阵,而动画数据的变化可以认为是那些节点的旋转。要知道的是,骨骼作用顶点必须保证骨骼变换到世界坐标系中。前面还提到bind_shape_matrix变换矩阵,这个是针对整个geometry进行的,也就是每一个顶点的变换。现在问题是骨骼如何权重顶点?

    我们知道一个顶点被多个骨骼权重影响,同时也就是一个骨骼作用多个顶点。看是否可以这样想象下,每块骨骼当做一个作用矢量,所有矢量作用加一起,就是最终顶点位置。在dx里面叫做所谓的顶点混合,公式如下:

    V_last = M_b1*V_origin*weight1 + M_b2*V_origin*weight2 + M_b3*V_origin*weight3+ M_b4*V_origin*weight4

    一般情况下,考虑一个顶点受到4个骨骼Joint作用应该就够了,毕竟低端显卡也只能满足这个。通过计算每一个顶点,得到最后的顶点位置,然后上传渲染。

    上面的分析很容易使用软件的方面实现,如何考虑使用stage3D进行硬件渲染骨骼动画呢?思考一下,我们需要写一个shader实现一个顶点被四个矩阵变换,va顶点寄存器一共8个。一个矩阵必须4个寄存器,明显不够用。v变量寄存器8个,显然你没法传递值,只能打vc常量寄存器的主意了。我的做法是使用四元数代替matrix,1个寄存器就够了,必须注意的平移信息必须使用一个寄存器保存,也就是共使用2个寄存器,节约50%。将所有骨骼的变换四元数,加平移上传到vc中,vc一共128个,有几个是占用的,我这里只能使用59个,也就是限制了骨骼数量59块了。在顶点中,需要提供权重信息,和骨骼的index,最多4块骨骼作用一个顶点,必须占用2个va寄存器,一个放权重信息,一个放骨骼index。这里还必须考虑一个问题,毕竟有的顶点不受到那么多骨骼作用,也可能实际没有受到作用,这里必须进行一个补齐了,否则shader没法写了!

    现在搞shader,通过va中存放的骨骼index,获取具体是那个vc。话说agal是很低级的东东,其实你错了!如下:

    mov v0, vc[va6.x]

    agal支持相对寻址,卧槽,本以为我的思路错误了,没想到能这样搞,顶礼膜拜adobe!其中va6.x是骨骼index,vc[va6.x]就能取得那个骨骼四元数了。具体shader参看源代码!

    需要注意的,agal最多200条操作代码,所以你得考虑效率、行数了。可以采用分离顶点数据,然后分批上传顶点、骨骼,理论上可以实现不限制骨骼数量。a3d的思路不知道是不是这样的?另外,agal调试确实是蛋疼,一看黑屏,神马也没有,也没法逐行调试。建议先把软件实现搞完美,剩下的问题就是书写上的错误了。

    5 结束

    通过上面的分析,你应该比较清楚了dae文件格式,当你对比代码,自己动手书写的时候,或许能领悟更深!当然dae还有其他一些描述,如光照、摄像机,蒙皮嵌套等,这里都没有说明,将在后续考虑。毕竟革命尚未成功,需要继续奋斗!文章由harry书写,转载请注明出处,谢谢。源代码,链接:

    download


    参考

    http://www.wazim.com/Collada_Tutorial_1.htm
    http://blog.csdn.net/qyfcool/article/details/6775309

    http://www.the3frames.com/?p=788


    http://www.the3frames.com/?p=788


    展开全文
  • SceneKit是ios8之后苹果推出了一个3D模型渲染框架。 SceneKit现在可以支持有限的几种模型,截止到我写这篇文章为止似乎只有.dae和.abc后一种模型我没有使用过。这篇文章只针对.dae模型写。 首先如果是希望加载一个...


    SceneKit是ios8之后苹果推出了一个3D模型渲染框架。

    SceneKit现在可以支持有限的几种模型,截止到我写这篇文章为止似乎只有.dae和.abc后一种模型我没有使用过。这篇文章只针对.dae模型写。
      首先如果是希望加载一个已有的,不需要程序在运行的时候动态添加的dae模型。那么我们可以直接新建一个game类型的工程。在选项中选择SceneKit,在程序中加载自带模型的那句话中将模型名称替换即可。本文主要讲一下如何导出dae模型,并在server端动态下载并显示。
      首先我们手中有一个.stl或者其他的模型文件,将模型文件转换成.dae文件我使用Blender。
    blender for mac版下载 我放在我的GitHub上需要自行下载:https://github.com/baitongtong/Blender.git
      (1)在Blender中新建场景
      (2)在右上侧栏中将自动生成的Cube、Camera等3个物体删掉
      (3)导入我们已有的模型文件
      (4)调整我们的模型文件的方向、大小
      (5)在右上侧栏更改模型文件及子文件的名字为你要导出的dae文件的名字(这一步很重要!)
      (6)在左侧栏中Edit Options中点击Smooth
      (7)File->export->dae
      (8)在接下来的页面中,我们选择导出的位置和文件的名字,并且在左侧选项Texture中选择include material texture(同样重要!)
      接下来我们在桌面上新建一个文件夹,暂时起名为model,更改后缀为.scnassets,将我们生成好的模型文件拷贝进去。SceneKit对于动态添加文件夹写了两个脚本。不太清楚作用原理是什么,以后再研究吧。暂时知道怎么用就行。将copySceneKitAssets、scntool文件拷贝到model.scnassets所在的目录下,进入终端并cd到该目录下,运行
    1 ./copySceneKitAssets model.scnassets -o model-o.scnassets

    如果终端没有报错,并且生成了model-o.scnassets,则代表运行成功。
      接下来我们把生成的model-o.scnassets文件打包zip文件,目的是为了能让iPhone客户端下载的时候文件更小。
      打包好了之后上传至服务器即可。
      两个可执行文件下载链接 http://download.csdn.net/detail/u013588047/8937773  

      接下来是重头戏,如何在程序中下载,解压,并显示呢。
      下载解压我使用了两个开源框架 AFNetworking 和 SSZipArchive ,朋友们可以自行查阅使用方法。
      一步一步来,先是下载,解压

    - (void)downloadZip {
        
        NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
        AFURLSessionManager *manager = [[AFURLSessionManager alloc] initWithSessionConfiguration:configuration];
        //这里我们用本地链接替代一下,可以使用任意url链接
        NSURL *URL = [NSURL URLWithString:@"file:///User/name/Desktop/model.scnassets.zip"];
        NSURLRequest *request = [NSURLRequest requestWithURL:URL];
        
        NSURLSessionDownloadTask *downloadTask = [manager downloadTaskWithRequest:request progress:nil destination:^NSURL *(NSURL *targetPath, NSURLResponse *response) {
            NSURL *documentsDirectoryURL = [[NSFileManager defaultManager] URLForDirectory:NSDocumentDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:NO error:nil];
            return [documentsDirectoryURL URLByAppendingPathComponent:[response suggestedFilename]];
        } completionHandler:^(NSURLResponse *response, NSURL *filePath, NSError *error) {
            NSLog(@"File downloaded to: %@", filePath);
            
            //对文件解压
            NSArray  *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
            NSString *documentsDirectory = [paths objectAtIndex:0];
            NSString *inputPath = [documentsDirectory stringByAppendingPathComponent:@"/product-1-optimized.scnassets.zip"];
            
            NSError *zipError = nil;
            
            [SSZipArchive unzipFileAtPath:inputPath toDestination:documentsDirectory overwrite:YES password:nil error:&zipError];
            
            if( zipError ){
                NSLog(@"[GameVC] Something went wrong while unzipping: %@", zipError.debugDescription);
            }else {
                NSLog(@"[GameVC] Archive unzipped successfully");
                [self startScene];
            }
            
        }];
        [downloadTask resume];
    }

     


    而对于3d模型场景的创建,我们使用SCNSceneSource,代码如下
    1 NSURL *documentsDirectoryURL = [[NSFileManager defaultManager] URLForDirectory:NSDocumentDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:NO error:nil];
    2//这里的dae文件名字是我们导出时定义的文件名,下面一段代码中加载的SCNNode是我们之前在面板中改过的模型名
    3 documentsDirectoryURL = [documentsDirectoryURL URLByAppendingPathComponent:@"model.scnassets/cube.dae"];
    4
    5 SCNSceneSource *sceneSource = [SCNSceneSource sceneSourceWithURL:documentsDirectoryURL options:nil];
    然后我们加载.dae文件中的模型,作为一个SCNNode,名字为我们在一开始改过的模型名
    1 SCNNode *theCube = [sceneSource entryWithIdentifier:@"Cube" withClass:[SCNNode class]];

    最后我们设置一下灯光等效果,其实是新建game文件中设置好了的,我们要做的是将SCNNode *theCube加载到Scene中
    // Create a new scene
    SCNScene *scene = [SCNScene scene];
        
    // create and add a camera to the scene
    SCNNode *cameraNode = [SCNNode node];
    cameraNode.camera = [SCNCamera camera];
    [scene.rootNode addChildNode:cameraNode];
        
    // place the camera
    cameraNode.position = SCNVector3Make(0, 0, 15);
        
    // create and add a light to the scene
    SCNNode *lightNode = [SCNNode node];
    lightNode.light = [SCNLight light];
    lightNode.light.type = SCNLightTypeOmni;
    lightNode.position = SCNVector3Make(0, 10, 10);
    [scene.rootNode addChildNode:lightNode];
        
    // create and add an ambient light to the scene
    SCNNode *ambientLightNode = [SCNNode node];
    ambientLightNode.light = [SCNLight light];
    ambientLightNode.light.type = SCNLightTypeAmbient;
    ambientLightNode.light.color = [UIColor darkGrayColor];
    [scene.rootNode addChildNode:ambientLightNode];
    
    // Add our cube to the scene
    [scene.rootNode addChildNode:theCube];
    
    // retrieve the SCNView
    SCNView *scnView = (SCNView *)self.view;
    
    // set the scene to the view
    scnView.scene = scene;
    
    // allows the user to manipulate the camera
    scnView.allowsCameraControl = YES;
    
    // show statistics such as fps and timing information
    scnView.showsStatistics = YES;
    
    // configure the view
    scnView.backgroundColor = [UIColor blackColor];

     


    这样我们就可以动态下载一个dae文件并显示了。

    转载于:https://www.cnblogs.com/baitongtong/p/5884647.html

    展开全文
  • iOS-数据归档

    2018-10-06 04:06:19
    目期及须次论值率于直己电断很听件按张量系太已统育建高和广她代达会头矿前们便始江才间据参科界空示志特合向般革石并四第从然出府界工存科候速...料器极包选育压最具身约区写统元验真花商重人约道准治断至济观运系...
  • 我是按照“坤小”博客里的文章一步步的,文章写得清晰易懂,所以搬运给大家,但是由于我不喜欢这个默认的飞机,我想换一个模型,换成我的周瑜大人,所以产生了我的Demo:ARKit入门周瑜模型,我自己提取的模型,...
  • 1.开启 terminal 2.移除现有 Ruby 默认源 $ gem sources --remove https://rubygems.org/ 3.使用新的源 $ gem sources -a ... 4.验证新源是否替换成功 $ gem sources -l 5.安装 CocoaPods $ sudo gem insta
  • hello ,大家好,我是Roc.Tian,最近一直在研究...1,最简单的一种方法,直接打开dae 或者 scn 文件直接设置 如上图,这个dae 文件中只有一个几何体,几何体中只有一个材质球,然后设置材质球的diffuse 属性就可以了。
  • ios逆向

    2013-11-18 14:07:39
    iOS (CocoaTouch)的各私有API都可以通过runtime查看获得,您可以自己写个methodbrwoser。如果觉得麻烦的话可以到Github看现成的,我收藏了俩: https://github.com/kennytm/iphone-private-frameworks 和 ...
  • iOS 部分问题总结

    2013-04-12 21:23:07
    最近在iOS一些事情,稍微记录下 1. 调用邮件客户端,或打开网页  都是调用[[UIApplication sharedApplication] openURL,传的参数不同而已。  注意前者在模拟器上是看不到的。貌似是因为模拟器上没有邮件...
  • 利用ARKit制作一个类似哆啦A梦的AR任意门,iOS平台,使用Swift语言。
  • 我们本次研究的目的是让iOS系统在无需事先或在启动过程中修复内核的情况下顺利启动,使用新模块扩展QEMU执行arm64 XNU系统的功能,并获得交互式bash shell。我们会在本文中介绍如何在QEMU上执行iOS并启动一个交互式...
  • iOS野指针定位总结

    2019-06-11 14:42:46
    成因 野指针就是指向一个已删除的对象或者受限内存区域的指针。 我们写C++的时候强调指针初始化为NULL,强调用完后也为其赋值为NULL,谁分配的谁回收,来避免野指针的问题。...在iOS9之前,系统库的delegat...
  • iOS 开发怎么入门?

    2016-08-06 13:16:26
    这次更新把大多数推荐书籍及网站的图贴了出来,又给大家介绍了很多iOS新的学习资源,我觉得这已经是知乎上最权威的iOS教学资源锦集了吧,令人震惊的是居然有人会反对这篇答案。其实这篇回答已经远远
  • 1,最简单的一种方法,直接打开dae 或者 scn 文件直接设置 如上图,这个dae 文件中只有一个几何体,几何体中只有一个材质球,然后设置材质球的diffuse 属性就可以了。 2,代码设置,还是针对这个最简单的dae 文件 ...
  • NSHomeDirectory() 获取到当前APP的沙盒路径.. 沙盒路径下有三个文件夹: (1) temp ->就像命名一样,存放临时文件,在APP关闭再启动后就没有了,不能放重要的东西. (2) Documents ->除非APP卸载,不然里面东西是不会...
  • 上次总结了多线程的用法,这次再复习下的iOS动画的东西。这次依然先是以API为主,因为好多人还是API好多的东西还不会用。然后中间穿插些例子,例子和代码文章中都会有。因为篇幅比较长,先列一下大纲。 动画的继承...
  • 本文只介绍,项目源码在github,链接如下 JSAnalysis项目github链接 项目简介 JSAnalysis 是一款苹果设备信息静默获取的工具。它可以在用户无感知的情况下获取到尽可能多的设备信息,方便开发者对用户提供更好的体验...
  • 在把Unity打包成IOS安装包时,经常需要自动化设置导出工程的各项配置。 在Unity 4.x时代,常用的工具是XUPorter,在Unity 5.x之后,Unity官方提供了另外一套工具,叫做xcodeapi,因为是官方维护的,所以在使用上...
1 2 3 4 5 ... 14
收藏数 277
精华内容 110
关键字:

.dae怎么做 ios