现在游戏引擎都必须unity 多线程渲染染么

翻译:王成林 ( 麦克斯韦的麦斯威尔 )
审校:黄秀美(厚德载物)
  在本文中我将为你展示在UE4中使用C++实现多线程有多么的容易。直接援引维基百科相关页面的介绍,多线程是一个CPU同时处理多个进程或线程的能力。
  现代软件应用被设计为可以在任何时间下都能通知用户它们的状态。例如,如果一个应用在某处卡住了,它极有可能显示一条相应的指示(例如一个进度条或者一个载入环)。一个应用的逻辑通常被分为两个(或多个)线程。主线程永远负责应用的UI,因为它可以显示或隐藏所有进度指示,而其它线程在“背景”中运行它们的逻辑。
  因为软件应用中的主线程负责UI,你可以将虚幻引擎4中主线程的功能理解为负责渲染画面。基于这点,我们将它称作游戏线程。
  如果你要在游戏线程中进行繁重的运算,你的游戏很有可能会卡死(取决于你的PC以及你进行的运算)。在本文中,我将建立一个简单的函数,它能够寻找前N个质数(N在Editor进行设置)。另外,我创建了两个输入事件——一个在游戏线程中调用该函数,另一个在一个不同的线程中调用该函数。
  在我们开始前,先看看最终结果:
  建立你的项目
  在本教程中我使用的是第三人称C++项目模板。在角色的头文件中我加入了以下代码:
  然后在角色类的头文件中,在角色类的声明和实现的后面我加入了以下命名空间,它包含了实际进行计算的函数:
  后面我们将在角色的头文件中(注意不是在角色类中)加入更多的类。我们刚刚声明了一个包含静态函数CalculatePrimeNumbers的命名空间,这样就能从不同的代码类中使用同一个函数了。
  这是CalculatePrimeNumbers函数的实现:
  为CalculatePrimeNumbersAsync函数添加一个空的实现然后编译并保存你的代码。然后如下图所示,在蓝图中设置两个按键绑定:
  创建一个任务
  当提及多线程时,你会听见很多关于任务(Task)的内容。简单来说,一个任务就是一段可以在任意线程上运行的代码。我们将创建一个新的类,它将在另外一个线程中执行CalculatePrimeNumbers函数。
  为了添加该类,我们不需要像平常那样通过UE4编辑器添加一个C++类。我们将手动添加我们的类!
  那么这一次我们将继承哪个类呢?嗯,我们需要一个内置了创建和使用任务功能的类。我已经搜索了引擎的源代码并且找到了所需的类,所以你不用担心!
  紧接着你的命名空间的声明,添加以下类:
  添加完以上代码后,在CalculatePrimeNumbersAsync中加入以下实现:
  编译并测试你的代码!别忘了通过编辑器调整MaxPrime变量以得到和上面视频中相似的结果!
  如果你凌乱了……这是我的角色类头文件的全部内容:
  PS:如果你想更加详细地了解UE4中的多线程功能,我建议你仔细钻研引擎源代码中的AsyncWork.h文件。
  【版权声明】
  原文作者未做权利声明,视为共享知识产权进入公共领域,自动获得授权。
  点击一下
  立即阅读近期热文
添加小编微信,发送“程序”,可享双重福利
1.加入GAD程序猿交流基地
获取行业干货资讯
观看大牛分享直播
2.直接领取60G独家程序资料库
腾讯内部分享、文章教程、视频教程等
↓长按添加小编GAD-沫沫↓
  点击“阅读原文”
声明:本文由入驻搜狐公众平台的作者撰写,除搜狐官方账号外,观点仅代表作者本人,不代表搜狐立场。&&&&&&&&&正文
给得再多不如懂我 实测多线程处理器对游戏的影响
02:25:47&&&&来源:游侠硬件&&&&编辑:Kumamon&&&&
  对电脑硬件稍有接触的人应该不时听到电脑硬件性能过剩这种趋势。可以说有一定道理但不能通俗的说所有的电脑硬件。至少电脑硬件三大件中的显卡几乎就没有性能过剩的时期。但对于电脑的大脑CPU来说,对于普通日常应用、专业应用(渲染设计除外)、大型游戏都能轻松胜任。网上有不少玩家高呼什么目前游戏对多线程处理器、超线程优化不到位,到底在游戏中近年来的高端或者旗舰CPU是否真的过剩?超线程在游戏当中又是否真的成负担?框框多不代表游戏就一定吃得饱吃得好  实测出真知,作为图拉丁吧的粉丝,笔者今天所要使用的是曾经风靡全吧第二代桌面级旗舰处理器i7 3960X。Sandy Bridge-E架构,具备6核12线程(超线程)、不锁倍频自由超频设计。即使放到今天来看规格也并不渺小。由于该款处理器多达6个物理核心,因此用于测试游戏对多核处理器的需求程度是相当具有参考性的。盲僧啊盲僧,你能否摸到我的主机有几个蛋  另外为了缩短Sandy Bridge-E架构相落后于目前最新架构的“牙膏”差距,笔者将它超频至于4.1GHz。顺带能查看一下在处理器线程较少的时候较高的主频能否挽回一定的劣势。测试方法非常简单:处理器设定具体分为7种模式:6核12线程、6核6线程、5核5线程如此类推,直到最后化身单核处理器,看看他们各自在游戏中的表现如何。  测试平台配置介绍以及游戏硬件资源占用情况测试平台列表硬件名称CPUIntel Core i7 Mhz=4.0Ghz主板Intel Z170主板内存DDR4-硬盘Galaxy 240GB SSD希捷 TB参测显卡微星GTX 960&&GAMING 4G (1127MHz/7010MHz,Boost:1304MHz)软件信息操作系统Windows 10 TH2 (X64_CHS专业版)驱动版本NVIDIA 365.10-Desktop-Win7-64bit-international-whql测试平台规格(点击查看大图)  测试平台说明,本次测试平台并没有采用1080P分辨率的常规设定一方面是由于笔者采用的2G显存版本的GTX960,如果使用1080P在全高特效下难免部分游戏会出现性能轻微爪鸡的情况,为了有限条件内最大限度消除显卡方面的瓶颈,笔者采用720P分辨率显示器。  实测数据 游戏对多线程不是十分敏感  从上述一款单机两款网游来看,6核12线程在性能上与4核4线程设定差距十分渺小,甚至可以说在3核3线程模式上性能已经基本处于够用状态。再往上增加核心数量游戏的帧速提升可谓微乎其微。也难怪很多玩家在给新人推荐配置时往往推荐I5处理器+GTX970显卡这样的搭配。毕竟四核就够用了。而CF表现如此奇葩的原因在于CF采用较老的游戏引擎制作,因此对多核处理器的优化不是十分到位。反观LOL,更多的处理器线程数可以让游戏帧速表现更为出色。
剁手党推荐
热门游戏推荐4264人阅读
技术理论(1005)
其它文章(1414)
Direct3D(476)
OpenGL(338)
GPU(293)
&&首先我们得明确3D引擎使用多线程的目的所在:
1、在CPU上进行的逻辑计算(比如骨骼动画粒子发射等)不影响渲染速度
2、较差的GPU渲染速度的低下不影响逻辑速度
&&&&& 第一个目标已经很明确了,我来解释下需要达到第二个目标的原因:许多动作游戏的逻辑判定是基于帧的,所以在渲染较慢的情况下,逻辑不能跳帧,而仍然需要严格执行才能保证游戏逻辑的正确性,这就导致了游戏速度的放慢,而实际上个人认为渲染保持15帧以上就已经可以正常进行游戏了。
&&&&&&在较差的GPU上跑《鬼泣4》《刺客信条》《波斯王子4》简直就像是慢镜头一样,完全没法玩。而实际上CPU跑满帧是没有问题的,如果能把逻辑帧和渲染帧彻底分离,即使渲染帧达不到要求,但CPU仍能正确的执行游戏逻辑,就可以解决动作游戏对GPU要求过高的问题。
&&&&& 我们先来看多线程Ogre的两种架构,第一种是middle-level multithread
&&&&& 如上图所示,每个需渲染的实体被复制成了两份,主线程和渲染线程交替更新和渲染同一个实体的两个备份,并在一帧结束时同步,这种解决方案达到了第一个目标而并没有达到第二个目标,同时两份实体的维护也相对复杂,并且没法为更多核数的CPU进行扩展优化。
&&&&& 第二种Ogre多线程的方法是 low-level multithread
&&&&& 如图,将D3D对象复制两份,同样是在帧结束时同步并交换,和上面的优缺点类似。两种多线程Ogre的解决方案都是在引擎层完成的,对上层应用透明,对于用户而言无需考虑多线程细节,这点是非常不错的。
&&&&& 接下来我们来看SIGGRAPH2008上,id soft提出的多线程3D引擎的方案
&&&&& 这里是已PS3的引擎结构为例的,与PC有较大的差别,其中SPU是Cell芯片的8个协处理器,拥有强大的并行能力,id的解决方案在SPU上进行了诸如骨骼动画、形变动画、顶点和索引缓存的压缩、Progressive Mesh的计算等诸多内容,同时与PPU上的物理计算RSX上的渲染工作交错进行,最大化的利用了PS3的硬件结构,最终的游戏产品《Rage》很快就会面世了!
&&&&& 最后是我的解决方案
&&&&& 特点是逻辑完全分离,无需同步,虽然成功的达到了文章开始提出的两个目标,但对于引擎的使用者必须考虑多线程的诸多问题,各种计算需放在哪个线程,如何在两个线程间交互,都需要深入思考,所以要应用到实际的游戏制作,恐怕还有很长的一段路要走。
&&相关文章推荐
参考知识库
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
访问:6453231次
积分:76983
积分:76983
排名:第18名
原创:71篇
转载:4305篇
评论:850条
声明:早期转载的文章未标明转载敬请原谅,以后将陆续改过来,向原创者致敬!
有问题可留言
痞子龙3D编程
QQ技术交流群:
(2)(3)(6)(10)(6)(19)(17)(17)(8)(5)(8)(14)(13)(3)(44)(42)(46)(40)(123)(114)(128)(159)(168)(40)(45)(43)(38)(5)(6)(7)(2)(3)(7)(24)(5)(5)(16)(17)(16)(66)(7)(55)(2)(37)(16)(1)(10)(6)(37)(5)(31)(18)(31)(128)(333)(203)(256)(59)(78)(57)(16)(39)(10)(27)(16)(8)(26)(32)(53)(56)(45)(142)(228)(6)(10)(6)(9)(6)(9)(22)(25)(18)(83)(208)(442)(111)(32)(1)游戏性能=渲染速度?【cpu吧】_百度贴吧
&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&签到排名:今日本吧第个签到,本吧因你更精彩,明天继续来努力!
本吧签到人数:0成为超级会员,使用一键签到本月漏签0次!成为超级会员,赠送8张补签卡连续签到:天&&累计签到:天超级会员单次开通12个月以上,赠送连续签到卡3张
关注:302,324贴子:
游戏性能=渲染速度?收藏
以前的游戏配置再低也很少卡 还可以多开 现在3d一出来各种配置不够用 是不是CPU渲染速度就基本代表CPU的游戏性能了?如果正确怎么会有那么多单核双核四核游戏的说法?引擎关系?
电脑配件,正品行货低价促销!货到付款,全国联保,满99元免运费!全场超低价格,货到付款,配送极速,京东给您不一样的购物新体验!
游戏性能不等于渲染。一个是吃单线程性能,一个是多线程。所谓核心优化,实际还是线程优化。
可是我用以前的a6本子玩剑灵跟室友的二代i5效果一样 甚至还要流畅点
我用r11.5测多核分数差不多 单核我才接近0.5分 他的都有1.1分。显卡也都差不多
处理器就是处理游戏数据。处理完了图形方面交给显卡运算完成,显卡就是煞笔。a卡就一个指令集。无脑算无脑的听处理器指挥,n卡3个指令集。所以矿卡用a卡
剑灵,这个游戏优化太差了说明不了什么问题。我的90+750TI最低帧数差不多。
以前是2d现在是3d
你说的四核游戏和双核游戏那是支持多线程优化,,也就是说让你全部核心都能用上。这样就不会出现奔腾玩游戏和I3没区别的问题了、、
得看那游戏吃cpu还是gpu了,还是都吃
毕竟2d游戏基本都是不需要配置的 需要渲染的地方也很少
3d就处处都要渲染
重要点依次是开发的水平和投入,引擎的优劣,CPU负责建模,渲染,计算物理AI所制造的最低帧,显卡的特效。所以看到现在低U高显的配置就蛋疼
登录百度帐号推荐应用
为兴趣而生,贴吧更懂你。或努力加载中,稍等...
暂无新消息
努力加载中,稍等...
已无更多消息...
这些人最近关注了你
努力加载中,稍等...
已无更多消息
努力加载中,稍等...
已无更多消息
游戏引擎多线程模型渲染解决方案
版权所有,禁止匿名转载;禁止商业使用;禁止个人使用。
前言最近一直在做项目优化,可是由于项目引擎历史原因,不能去砍掉某些功能而去优化项目,项目开发到这种程度,只能在这个基础上去提升整个引擎的效率,常规的CPU和GPU上的优化(美术资源上的缩减,CPU上耗费地方和GPU耗费地方的优化等)基本上都做了。当然每个人都希望自己的游戏跑的越快越好,现在大部分机器都已经至少是双核的,如果能发挥多核优势,游戏的速度会大幅提升。这里只是谈游戏引擎的多线程,至于游戏逻辑和游戏引擎这面关联不大。游戏中大部分线程一个是用来每帧更新,一个是资源加载。资源加载本文不谈,但下面设计的多线程架构会考虑多线程加载的情况,让多线程加载无缝对接。&多线程模型下面一共想了2种多线程框架,这2种都不是那种无休止的那种让每个线程都疯狂的运行,这样处理起来会有很多棘手的问题,为了简化问题,需要每帧都去同步一次这些线程,这样在提升效率的同时也简化问题的复杂程度(其实这2种模型原理基本一样,实现细节不同,因为一个是渲染,一个是纯引擎更新)。&游戏引擎一把流程分成下面这几部分:&流程1:游戏物体的update流程2:Cull阶段,这个阶段包括相机对物体的裁剪流程3:对可见物体的渲染分类流程4:那些可见并且依赖相机更新的物体进行更新。流程5:渲染&可能不同游戏引擎流程处理和上面不太一样,但大多数都差不多。这里面每一个过程都是相互依赖的,上一个流程输出,是下一个流程输入,一般只要是相互依赖的,要想做多线程处理,每一帧去同步的话,都要有2个buffer,上一个流程用一个buffer把上一帧的结果记录下来,下一个流程去取另一个buffer进行处理,然后帧末或者帧前交换2个buffer。现在提出2种多线程模型,虽然不能让每个流程去多线程出来,但也可以尽量发挥多个CPU的能力,总之比单一线程来跑还是快的。模型1模型1的机制其实很简单,把渲染部分单独拿出来,但由于渲染部分和上面流程是相互依赖的,这个时候必须用双缓冲buffer,做延后一帧处理。也就是上面说过的,一个buffer是给主线程用来填充的渲染数据,另一个是用来给渲染线程来渲染的,然后再步骤2的时候同步2个线程&模型2模型2比模型1的改进就是把流程1分解成多个线程处理,一般游戏里比较消耗的更新就是骨骼动画和粒子的更新。如果更新是没有依赖关系的,就可以把它放到一个单独线程里来出来,如果update m 和update n 有依赖关系,但update m 和update n的集合和其他没有依赖关系,那么就把他们放到一个里面去更新。要把这个划分出来除了设计上就要考虑最小依赖,还要去考虑依赖程序,才能准确划分。举个例子,有些粒子是绑定到骨头上的,骨头的更新后才能粒子更新,因为粒子要跟随的,如果再无其他更新依赖那么就可以把它们弄到一个线程去。有些粒子不绑定到骨头上,而且这样就可以把它弄到一个线程去。游戏世界中每一个Actor的更新保持独立性,这种独立性是需要制约的,因为我们很难保证Actor的独立性,要保证独立性并不是太难,需要做一些额外的工作。我举个例子比如,场景中有2个物体,一个游乐场里面的旋转木马和一个人,默认情况下,人不在木马上,并且旋转木马还在转,人的更新和木马的更新是毫无依赖关系的,他们每一个都可以单独放到一个线程去更新。这个时候如果人上了旋转木马,人就要跟随着旋转木马来动,简单的只是位置变,复杂的可能人被绑定到旋转木马的骨头上,跟着骨头走。这个时候你就不能把他们分别放到一个线程去更新,你要把他们放到同一个线程去更新,把他们看做一个整体。一般情况下,每个Actor只是逻辑数据,它的实际渲染数据作为一个node封装在Actor里面,因为我们考虑的是引擎的多线程,不考虑逻辑层面,当人和旋转木马独立的时候,人和旋转木马的node都attach在引擎里的world,而人和旋转木马的actor都在逻辑层面的world,如果要想很好解决这个问题,我们就要让直接attach&引擎world&里面&node&都是独立的,那个node&不独立于另一个,则这个node&必须detach&下来,在重新attach&到另一个node上。就刚才的例子,人的node要想上旋转木马,必须从引擎world上detach下来,然后再重新attach木马的node上,这样引擎world只剩下一个旋转木马node,当旋转木马node更新的时候,人的node作为子节点,去更新。这就保证了直接attach&引擎world&里面&node&都是独立的,而逻辑层面还是没有变。还有一种方法就是,你在更新前,把所有相互依赖的归类,这个相对引擎而言,改动是比较小的,需要添加代码就可以/还有一种就是把骨骼动画,粒子独立出来。但这种方法,需要考虑的因素可能会很多,粒子和骨头都有可能一个绑在另一个上,谁先更新呢?这个你又要多费劲,去想方设法的改你架构分几种情况,而且有可能还要做帧同步等等,十分麻烦。其实流程2也可以独立出来做多线程,因为场景里面会有多个相机,把每个相机的更新仍给一个线程,但必须等流程1所有都更新完毕,只需要同步一次即可。然后等所有流程2更新完毕,再同步一次,再去做流程3,4.。这里只是说了大体架构和方法,其实模型2和渲染是没有关系的,但相对渲染来讲是很简单的,如果渲染多线程处理好了,非渲染上的多线程很好处理,所以这里只给出了原理,没有去实现。还有实现的时候,对于更新开辟一个线程就可以了,毕竟CPU核心是有限的,你是目的是让你的CPU跑满,而不是让它喘不过气,所以尽量不要用线程池。但如果你更新要用多线程,裁剪又要多线程,他们之间要做一次帧同步的,他们不可能同时运行,这个时候你就可以设计一个好的线程池,不去浪费线程资源。&多线程渲染详细的解决方案准备工作在做多线程渲染之前,确实做了好多准备工作。以前没有做过多线程的大量的代码,只是些过一些小的DEMO,大学里面学的《操作系统》确实给了最主要帮助,我还清晰记得PV操作是《操作系统》课程的一个核心章节,虽然windows编程里面有了event概念,但原理其实都是一样的,而且无论是关键区,互斥量,信号量,其实它们都是《操作系统》课程的信号量。再一个要提的就是“原语”,指的就是在执行过程中是不可以打断的,例如j= i +1这个变成汇编指令根据硬件的不同可能是1条指令,也可能是2条指令(至少要一个add指令 和一个 mov 指令)如果2条以上指令,那么它就是可以被操作系统打断,它所在线程挂起,这里之所以提及这个,就是因为,有时候,你设计认为它没有被打断,它运行其实不对,其实它是被打断的,如果运行对了,那是你运气好。 上面只是最基础的东西,多线程渲染比较复杂在,它和D3D要打交道,自己以前也想过怎么处理各种复杂的情况,上网查过很多资料,也看过别人写的多线程的demo。不得不说,有些确实可以解决多线程渲染问题,但集成复杂度太高,一种情况做一种处理,这肯定要累死你,还有的只给理论没有任何细节的东西,更别说demo,这种能不能做成其实都很让人去怀疑。归类始终是解决问题最好的办法,找到问题相似点,然后统一处理,但这个相似点似乎不是那么难找到。unreal的出现,问题有了起色,但只能说起色,因为这种方法好多地方其实都在用,只不过unreal&编码方式和特别,用宏封装了起来,还有一个最重要的方式,它把执行的代码包装到了类成员的成员函数(好多用类似&commad&的多线程,都用函数指针了,其实那种方式很乱,参数都都自己做了一个栈传来传去的)。更更重要的是,它把多线程渲染实现了,而且还用到大型项目里面,这其实是最有说服力的,因为有时候一个理论提出来,你没有大规模应用,别人肯定会怀疑你的理论,而你一旦实现了,他不会怀疑你的理论而是怀疑自己的脑子了。Unreal这种方法虽然好,但集成复杂度很高,你需要了解你自己现在引擎很多东西,而且还要考虑线程安全问题,后面我会说道这些问题。&多线程渲染为了简化问题,不可能让主线程和渲染线程毫无次序的运行,所以采用帧同步,让他们每帧去同步一次。主线程提交数据,渲染线程处理数据,当然这很容易就想到生产者和消费者的模式,这种模式需要有数据存放的地方,如果使用一个buffer存放数据,这个buffer的所有操作要用异步处理,防止2个线程同时对它进行操作,主线程有数据就放到这个buffer中,buffer里面只要有数据渲染线程就不停的处理。还有一种是用2个buffer,一个是主线程提交数据的buffer,一个是渲染线程处理数据的buffer,每帧结束后,交换2个buffer。使用2个buffer会多一些存储空间,但不会因为异步访问阻塞任何一个线程运行,而且只要渲染数据资源本身不是需要额外的空间,其实是不会浪费很多存储空间的,而且设计的好坏也会避免这样问题出现。&大体的框架就是:每帧开始的时候,主线程唤起渲染线程,2个线程一起运行,渲染线程处理完所有数据后会激活用来同步的的event,然后进入无限循环的状态,主线程去wait&这个event,如果渲染线程处理完了所有数据,主线程就不会被wait卡住,如果主线程先提交完数据,就会被wait卡住。一旦主线程通过wait&就挂起渲染线程,然后处理同步信息,包括交换2个buffer等。接下来是细节问题,这个处理数据buffer要怎么设计。采用unreal render command&作为buffer的基本成员,把每个要处理的数据封装成命令的形式,实际上就是一个类的实例,根据不同类型的要处理的数据,创建不同的类,然后实例化这个类,加入这个buffer中。为了简化这个过程,unreal&用一系列宏来封装了起来,提升了开发速度。#define&ENQUEUE_RENDER_COMMAND(TypeName,Params) ?&&&&&&&& { ?&&&&&&&&&&&&&&&&&&&check(IsInGameThread()); ?&&&&&&&&&&&&&&&&&&&if(GIsThreadedRendering) ?&&&&&&&&&&&&&&&&&& { ?&&&&&&&&&&&&&&&&&&&&&&&&&&&&FRingBuffer::AllocationContext&AllocationContext(GRenderCommandBuffer,sizeof(TypeName)); ?&&&&&&&&&&&&&&&&&&&&&&&&&&&&if(AllocationContext.GetAllocatedSize() &&sizeof(TypeName)) ?&&&&&&&&&&&&&&&&&&&&&&&&&&& { ?&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&check(AllocationContext.GetAllocatedSize() &=&sizeof(FSkipRenderCommand)); ?&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&new(AllocationContext)&FSkipRenderCommand(AllocationContext.GetAllocatedSize()); ?&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&AllocationContext.Commit(); ?&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&new(FRingBuffer::AllocationContext(GRenderCommandBuffer,sizeof(TypeName)))&TypeName&Params; ?&&&&&&&&&&&&&&&&&&&&&&&&&&& } ?&&&&&&&&&&&&&&&&&&&&&&&&&&&&else&?&&&&&&&&&&&&&&&&&&&&&&&&&&& { ?&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&new(AllocationContext)&TypeName&Params; ?&&&&&&&&&&&&&&&&&&&&&&&&&&& } ?&&&&&&&&&&&&&&&&&& } ?&&&&&&&&&&&&&&&&&&&else&?&&&&&&&&&&&&&&&&&& { ?&&&&&&&&&&&&&&&&&&&&&&&&&&&&TypeName&TypeName##Command&Params; ?&&&&&&&&&&&&&&&&&&&&&&&&&&&&TypeName##Command.Execute(); ?&&&&&&&&&&&&&&&&&& } ?&&&&&&&& }#defineENQUEUE_UNIQUE_RENDER_COMMAND_ONEPARAMETER(TypeName,ParamType1,ParamName1,ParamValue1,Code) ?&&&&&&&&&class&TypeName&:&public&FRenderCommand&?&&&&&&&& { ?&&&&&&&&&public: ?&&&&&&&&&&&&&&&&&&&typedef&ParamType1&_ParamType1; ?&&&&&&&&&&&&&&&&&&&TypeName(const&_ParamType1&&In##ParamName1): ?&&&&&&&&&&&&&&&&&& &&ParamName1(In##ParamName1) ?&&&&&&&&&&&&&&&&&& {} ?&&&&&&&&&&&&&&&&&&&virtual&UINT&Execute() ?&&&&&&&&&&&&&&&&&& { ?&&&&&&&&&&&&&&&&&&&&&&&&&&&&Code; ?&&&&&&&&&&&&&&&&&&&&&&&&&&&&return&sizeof(*this); ?&&&&&&&&&&&&&&&&&& } ?&&&&&&&&&&&&&&&&&&&virtual&const&TCHAR*&DescribeCommand() ?&&&&&&&&&&&&&&&&&& { ?&&&&&&&&&&&&&&&&&&&&&&&&&&&&return&TEXT( #TypeName&); ?&&&&&&&&&&&&&&&&&& } ?&&&&&&&&&private: ?&&&&&&&&&&&&&&&&&&&ParamType1&ParamName1; ?&&&&&&&& }; ?&&&&&&&&&ENQUEUE_RENDER_COMMAND(TypeName,(ParamValue1));&我只列出带一个参数的宏,通过ENQUEUE_UNIQUE_RENDER_COMMAND_ONEPARAMETER这个名字可以看出来,还有没有参数的,2个和3个的,其实只需要无参数和1个参数就足够,大多数参数封装到一个结构体里面作为一个整体,当作一个参数。#defineENQUEUE_UNIQUE_RENDER_COMMAND_ONEPARAMETER(TypeName,ParamType1,ParamName1,ParamValue1,Code)把代码展后这个宏什么意思一目了然。这个宏是定义了一个类,有一个参数,有个构造函数,通过外部的变量来赋值给里面的类成员变量。Execute()&这个是你要执行的代码,Code这个也是从宏传过来的。j = j + 1;ENQUEUE_UNIQUE_RENDER_COMMAND_ONEPARAMETER(Add,int,i,j,i++;)Class Add&:&public&FRenderCommand{public:typedef&int&_ParamType1;Add&(const&_ParamType1&&Ini):&:i(Ini){}virtual&UINT&Execute(){i++;return&sizeof(*this);}virtual&const&TCHAR*&DescribeCommand(){return&TEXT(&“Add”);}private:int}&在写宏的时候有一个变量j,在创建实例的时候,这个j就是构造函数的参数。#define&ENQUEUE_RENDER_COMMAND(TypeName,Params) ?这个宏是用来创建这个类的实例,然后把这个实例放入渲染队列,这可以看出这个如果是多线程渲染,则在一个内存空间中来创建这个实例,加入渲染队列,空间大小不够则加大空间,大体意思就是这样。如果不是多线程,则创建完实例直接运行。这里就出现一个很大的问题,这个东西要怎么使用,什么时候去使用,使用的时候要注意什么。继续上面的例子ENQUEUE_RENDER_COMMAND(Add,j);我这里就不全展开了,你展开就会得到实例化的代码。他的本意是把这些要处理的都扔到渲染线程去计算,先来看看unreal是怎么使用的,你搜索这个宏,可以看到,unreal在这上面的使用貌似没有什么成型的规范,大到整个裁减渲染,小到一个buffer copy&都被扔到渲染线程,唯一有点共性的就是这些都或多或少,或深或浅的和D3D有些关系,但这么说有点牵强,整个引擎都和D3D有关系,它也整个可以扔到渲染线程,如果你组织合理的话,确实是可以的,但效率能不能保证是另一个马事。再看下一个问题,使用这个宏的时候,要让使用者必须对封装的代码及其了解,因为这些要扔到渲染线程,必须要注意,这里面很可能有些数据是要被主线程使用,就可能涉及到线程安全问题,导致出各种诡异的问题。举一个例子:处理模型的骨骼问题,在渲染之前,要先更新骨架,把层级数据算好,把蒙皮信息的矩阵都要算好,渲染的时候把蒙皮的矩阵给VERTEX SHADER,这个时候就涉及到线程安全问题,这个存放蒙皮矩阵的地方,你要单独拷贝出来,存放然后让渲染线程来使用,这样才不会有线程安全的问题,保证渲染的时候这个数据不会被破坏。看看unreal是如何做的,展开它的render command宏,他的成员变量用来存放这个蒙皮信息矩阵的是一个array,构造实例的时候,会把主线程的array传到构造函数,来构造这个render command&实例,可能你一个地方这么用还好,如果大量这么用,数组之间的构造赋值,开辟空间,等这个command执行完后要析构,数组释放等等,这里会牺牲很多速度。&&&&&&&可以看到使用这个东西的时候,存在很多让人纠结的地方,就现在may引擎,你把那里代码扔到渲染线程里面,这个你要思考好多,还要主线线程安全,更改到多线程渲染,按照unreal方式是一个很耗费工程。&好的多线程的设计,应该是在引擎层和D3D层再有一层,这一层让使用引擎处理渲染问题时候去规避这些多线程风险。这个中间层,封装所有D3D,然后涉及到多线程的问题都在这一层处理,同时让引擎很容易就集成这种效果。Low level render Command,只是相对于high level render command&提出的,我把只封装D3D函数并且不涉及其他的都叫做low level render command&,那么其余都叫做high level render command。有些时候我需要一些组合D3D函数来达到效果,比如设置一个render target&,通常调用这个D3D函数的人,不会只调用它,还会先get&当前render target,保存住,然后在end render target&的时候你还要恢复回去,这个时候你要多个D3D函数集合成&来达到。这样看来unreal&里面基本上都是high level&的,至于D3D资源的创建,主线程创建就可以了,因为采用Low level render Command&你只有创建出来才会调用它,传递给渲染线程(这里还有资源创建和使用多线程问题,后面会详细说明)。&如果只用上面的方法,集成到引擎中,基本不需要架构修改,只需要添加代码即可,但用Low level render command&不能处理所有问题,就是D3D资源的LOCK问题,这个东西要和引擎层打交道,引擎更新的数据,要传到D3D资源&LOCK的buffer&中,如果想让使用引擎者对于lock是安全的,你就要封装lock&,里面再做多线程安全处理。有2种方法1.lock的时候挂起渲染线程,unlock的时候唤醒渲染线程2.创建双D3D资源。无论那种方法,只有资源是动态资源才会出现这种情况,如果开启多线程渲染,并且是动态资源,中间层都能处理。第一种方法实现最简单,但效率很低,对于粒子等大规模这种lock。当然你可以分类,统一集中一起处理,把所有动态资源都放到一起,这就增加了管理成本,本身挂起渲染线程就已经减少了效率,在这个过程之间,你的渲染线程不会进行任何数据处理,只能等到unlock结束后。第二种方法封装d3d资源的时候创建双D3D资源,每帧同步交换2个buffer,经验来讲这里占用多处理的存储空间不会是你内存瓶颈,速度还很快。第一种方法如果把动态资源分类的话,实现起来最简单的,虽然有速度损失,但也比单个线程跑快。分类的话,就需要有一个管理这些资源的系统。&&&&&&&第二种方法需要在封装D3D资源设计上做点考究了,架构上必然要去修改。速度块,改动小的方法,就是用high level render command,但这打破所有的都有一个中间层来规避线程安全的问题,引擎只需要把lock&相关的代码封装进去就可以。到现在为止还有一个问题没有解决,就是render command&线程无关的数据存放问题,unreal是用类成员变量来弄的,上面说过这个确实有很多问题。所以再管理command&同时再去管理一个内存分配问题,这个空间是事先分配好的,不够可以自动增长,无论render command&还是在这个过程中涉及到线程安全的都在这里面分配,每一个处理数据buffer都包含以一个这样的空间。还是刚才骨骼蒙皮的问题,我可以在当前buffer里面分配空间,封装render command&时候记录和这个空间的所有信息,比如起始地址,结束地址等等。上面这些基本可以解决所有问题,可以根据你自己引擎需要来改动。怎么样才改动方便。我除了实现上面几种方法外,还是实现一种dynamic mesh&的,这个也是一种mesh&它封装了&vb ib只和模型有关的信息,然后有二个内存空间,用来存放顶点数据,二个内存空间存放索引数据,说简单点,就是处理多线程时候,D3D&资源只有一份,2份内存资源来缓冲的,也是线程帧同步的时候交换这2个缓冲。这个dynamic mesh&分成4种,1.动态顶点,动态索引的(现在may引擎中的&debug draw&内部实现就是这种类型),2.动态顶点,静态索引(比如morph)3动态顶点&无索引的&4静态顶点,动态索引(地形lod&模型lod等)&这4种情况在游戏中都可以用到,不过这几种情况用high level render command&都可以实现,只不过用了这个可以封装出中间层,不用考虑线程安全问题。实际开发中遇到问题实战,理论归理论,实现归实现,在真正实现中,还是遇到大量的问题。1、首先有几个大方面的,第一就是自己不是多线编程高手,有时候多线程的并发,会有很多预想不到问题,你自己都想不到,有时候单CPU&并行运行&不会有问题,多个CPU并发就会出问题。死锁,饥饿,运行不对等是经常出现的。第二,对于这种自己没有经验的编程挑战,没有可以参考单元测试用例(unreal&那种是大游戏不是demo,所以细节部分有时候很难参考),最好先在小的单元测试上进行,否则直接在当前游戏上进行修改,问题很难跟踪,最后就只能以失败而告终。现在基本考虑了所有情况,在单元测试里面,即便说考虑到所有情况,也是相对的,在游戏里面出现问题还是有可能,但毕竟风险降低到最低了,所以你的测试用例尽量要覆盖所有情况。2、再说细节的方面,刚写完这个测试的时候就出现死锁,退出的时候游戏退不出去,还好,这个时候只是涉及到线程架构问题,没有涉及到内部render command问题,还比较好查。后来用了一个模型,来测试发现了线程架构的一个不同步问题,就因为一行代码写错位置,导致了这个问题。这种问题一般很难查,要求你对你写代码逻辑十分缜密,那些代码过程另一个线程也是可以在运行的要了如指掌(window&所有&阻塞和唤醒的内核对象,都是可以唤醒多次的,也就是说它里面引用计数是累加的,比如我event&激活它一次,如果你再调用一次激活,实际上你要调用2次reset&才可以,到不激活状态,内部不会判断如果已经是激活状态,让你不激活不作用)。3、封装render command&问题,大部分封装只要追寻上面的原则是没有问题的,我在封装set render target的时候&出现了一个问题,就是你要先get&当前render target&,然后你再处理其他的,当时写代码的时候&按照单线程处理&以为它已经获取到了,实际上只不过是交给了渲染线程队列里面,并没有再这里执行,所以根本就没有获取到。render target D3D debug&调试还给了错误信息,查了出来,depth scitencl buffer&没有给出错误信息,调试时打log比对才发现的。&&&&&& 4、还有个问题就是处理和视点有关的更新,本来更新和可见就是一个矛盾体,也就是说,更新了它不可见,则下一帧它就不更新,如果它可见了,就更新。这个时候用high level render command时候&如果考虑不好多线问题&就会出现图像跳变,在单线程可能延后一针没有问题,再多线程下可能就会有问题,例如在做地形LOD的时候,渲染数据可能和要渲染的个数没有匹配上,如果想差特别大,虽然只是一帧,也会有跳变,所以high level render command是给很高的多线程缜密思维的人用的,但它却是对引擎集成降低难度,真是很难取舍的一个东西。5、最后一个就是创建D3D设备的时候要用多线程标志,这个我刚开始知道有它,但文档上说它有效率损耗。总觉得这个方案的设计可以规避所有多线程问题,所以不使用这个标志也可以。这个在我自己家里的机器上测试没问题,可是公司的机器就是死锁,还有奔溃,而且没有堆栈。只怪自己不了解D3D内部怎么实现的了,只能用这个标志位了。后来发现,问题出现在资源创建和lock的地方,因为资源创建是在主线程创建的,lock是在渲染线程lock,虽然创建和lock不是同一个资源,但发现同时跑D3D也会有问题,创建资源是用的D3D device接口函数,Lock&资源是用资源的接口函数,本来不应该有什么冲突,可能是D3D里面做什么处理,要访问同一个东西,而导致问题。如果你的资源在渲染线程之前或者同步2个线程的时候都创建好,就不会有这个问题(事先创建好),但如果你做异步加载把创建资源扔到渲染线程,就可能要多考虑些问题了。我建议,游戏中无论怎么加线程,主线程还是作为一个中转站的作用,这样可以减少问题复杂程度。总之开了这个标志位会有性能损耗,但只要你创建资源不是始终都在和渲染线程并排的跑,应该问题不大,即使始终并排跑这种很少性能的减少,却可以规避设计复杂问题,也是值得的。&6、还有一个问题就是一旦主线程资源删除,而渲染线程还在用到这个资源,如果用智能指针去管理,首先智能指针要具备线程安全性,第二,如果渲染线程这个时候ref为1,这个command执行完后,要析构才能让这个资源删除,否则不析构永远无法删除,这里有个潜在问题,如果资源析构的时候调用了主线程的函数(例如资源管理中,析构后要从资源管理中删除等),这个时候线程安全性就无法保证,这里就太多不可确定。所以做一个资源GC功能很有必要,从这个资源ref为1(默认资源管理要保留一份,所以没人用的时候ref是1)的时候开始计时,这个时候没有其他在用,所以渲染肯定也不可见的,所以就不会进入渲染线程,到一定时间就可以把他GC掉,如果又有其他重新指向这个资源,那么把计时清0.7、最后一个就是分辨率切换,窗口切换,涉及到的&设备丢失问题。这个问题处理就是一旦检测到窗口切换和设备丢失(这个检测都是在主线程来响应的),马上就不要跑主线程的添加render command&和渲染线程,而是把2个缓冲buffer全都清空,去处理设备丢失问题。&代码说明和运行效果&&&&&&&说了这么多,对程序员来说,看到代码和实现效果比什么都是重要的,上面提到的问题以及方法,都被我实现过了。主多线程渲染架构&//如果设备不丢失,这里检测设备丢失,如果丢失先device lost&处理 然后返回false,下一帧在进这个函数后,再做device resetif(VSRenderer::ms_pRenderer-&CooperativeLevel()){VSRenderThreadSys::ms_pRenderTreadSys-&Begin();//通知渲染线程启动if&(VSSceneManager::ms_pSceneManager)&&& {&&&&&&&VSSceneManager::ms_pSceneManager-&Update(fTime);}//下面过程是添加render commandVSRenderer::ms_pRenderer-&BeginRendering();if&(VSSceneManager::ms_pSceneManager)&&& {&&&&&&&VSSceneManager::ms_pSceneManager-&Draw(fTime);}VSRenderer::ms_pRenderer-&EndRendering();&&&&&if&(VSRenderThreadSys::ms_pRenderTreadSys&&&&VSResourceManager::ms_bRenderThread)&&& {&&&&&&&VSRenderThreadSys::ms_pRenderTreadSys-&ExChange();//同步渲染线程,并交换buffer}else{//清空所有渲染render command&&&&if&(VSRenderThreadSys::ms_pRenderTreadSys)&&& {&&&&&&&VSRenderThreadSys::ms_pRenderTreadSys-&Clear();&&& }}//GC功能VSResourceManager::GC();void&VSRenderThreadSys::Begin(){//设置一个准备填render command buffer&&&&m_RenderThread.SetRender(m_RenderBuffer);&&& //启动渲染线程&&&&m_RenderThread.Start();}//渲染线程运行,只要不触发被迫停止,它就会一直运行下去,如果render command buffer的所有数据都处理完毕,马上提醒主线程,不再等待void&VSRenderThread::Run(){&&&&while(!IsStopTrigger())&&& {&&&&&&&if&(m_pRenderBuffer)&&&&&& {&&&&&&&&&&&m_pRenderBuffer-&Excuce();&&&&&&&&&&&m_pRenderBuffer&=&NULL;&&&&&& &&&&m_Event.Trigger();&&&&&& }&&&&& }}void&VSRenderThreadSys::ExChange(){//主线程等待渲染线程完毕&&&&m_RenderThread.m_Event.Wait();//挂起渲染线程&&&&m_RenderThread.Suspend();&&&&&&&m_RenderBuffer-&Clear();//交换2个buffer&&&&Swap(m_UpdateBuffer,m_RenderBuffer);&&& //有些资源有双D3D&资源的 进行多线程的,要交换buffer&&&&for&(unsigned&int&i&=&0&;&i&&&VSBind::ms_DynamicTwoBindArray.GetNum() ;i++)&&& {&&&&&&&VSBind::ms_DynamicTwoBindArray[i]-&ExChange();&&& }}RenderCommand&说明//这里封装了D3D&SetRenderStatebool&VSDX9Renderer::SetRenderState(D3DRENDERSTATETYPE&State,DWORD&Value){&&&&struct&VSDx9RenderStatePara&&& {&&&&&&&D3DRENDERSTATETYPE&State;&&&&&&&DWORD&Value;&&& };&&&&HRESULT&hResult&=&NULL;&&&&VSDx9RenderStatePara&RenderStatePara;&&&&RenderStatePara.State&=&State;&&&&RenderStatePara.Value&=&Value;&&&&&ENQUEUE_UNIQUE_RENDER_COMMAND_TWOPARAMETER(VSDx9SetRenderStateCommand,&&& &&&VSDx9RenderStatePara,RenderStatePara,RenderStatePara,LPDIRECT3DDEVICE9,m_pDevice,m_pDevice,&&& {&&&&&&&HRESULT&hResult&=&NULL;&&&&&&&hResult&=&m_pDevice-&SetRenderState(RenderStatePara.State,RenderStatePara.Value);&&&&&&&VSMAC_ASSERT(!FAILED(hResult));&&& })&&&&&&&hResult&=&m_pDevice-&SetRenderState(RenderStatePara.State,RenderStatePara.Value);&&&&&&ENQUEUE_UNIQUE_RENDER_COMMAND_END&&&&VSMAC_ASSERT(!FAILED(hResult));&&&&return&!FAILED(hResult);}&这里的宏和unreal内部实现不太一样,我做修改,基本意思就是如果开启了多线程渲染,则把命令提交到buffer中,如果不是则直接运行。所以你会看到2个hResult&=&m_pDevice-&SetRenderState(RenderStatePara.State,RenderStatePara.Value);其实第一个构建render command&的 类的 里面运行代码,第二是如果没开启多线程的话直接就运行。再来看这个宏,你要是仔细读前面unreal的,你就知道里面嵌套了一个宏,只有嵌套的不一样#define&ENQUEUE_RENDER_COMMAND(TypeName,Params) \&&&&if(VSResourceManager::ms_bRenderThread) \&&& { \&&&&&&&TypeName&*&pCommand&= (TypeName&*)VSRenderThreadSys::ms_pRenderTreadSys-&AssignCommand&TypeName&(); \&&&&&&&VS_NEW(pCommand)TypeName&Params; \&&& } \&&&&else&\&&& {如果是多线程的话,就构建实例,加入队列,如果不是,则直接运行代码#define&ENQUEUE_UNIQUE_RENDER_COMMAND_END&}再看一个bool&VSDX9Renderer::SetVertexShaderConstant(unsigned&int&uiStartRegister,void&*&pDate,&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&unsigned&int&RegisterNum,unsigned&int&uiType){&&&&struct&VSDx9VertexShaderConstantPara&&& {&&&&&&&unsigned&int&uiStartRegister;&&&&&&&void&*&pDate;&&&&&&&unsigned&int&RegisterNum;&&&&&&&unsigned&int&uiType;&&& };&&&&HRESULT&hResult&=&NULL;&&&&VSDx9VertexShaderConstantPara&VertexShaderConstantPara;&&&&VertexShaderConstantPara.uiStartRegister&=&uiStartRegister;&&&&VertexShaderConstantPara.RegisterNum&=&RegisterNum;&&&&VertexShaderConstantPara.uiType&=&uiType;&&&&if&(VSResourceManager::ms_bRenderThread)&&& {&&&&&&&VertexShaderConstantPara.pDate&=&VSRenderThreadSys::ms_pRenderTreadSys-&Assign(uiType,RegisterNum);&&&&&&&VSMemcpy(VertexShaderConstantPara.pDate,pDate,RegisterNum&*&sizeof(VSREAL) *&4);&&& }&&&&else&&& {&&&&&&&VertexShaderConstantPara.pDate&=&pDate;&&& }&&&&&&&&ENQUEUE_UNIQUE_RENDER_COMMAND_TWOPARAMETER(VSDx9SetVertexShaderConstantCommand,&&&VSDx9VertexShaderConstantPara,VertexShaderConstantPara,VertexShaderConstantPara,LPDIRECT3DDEVICE9,m_pDevice,m_pDevice,&&& {&&&&&&&HRESULT&hResult&=&NULL;&&&&&&&if(VertexShaderConstantPara.uiType&==&VSUserConstant::VT_BOOL)&&&&&& {&&&&&&&&&&&hResult&=&m_pDevice-&SetVertexShaderConstantB(VertexShaderConstantPara.uiStartRegister,(const&BOOL*)VertexShaderConstantPara.pDate,VertexShaderConstantPara.RegisterNum);&&&&&&&&&&&VSMAC_ASSERT(!FAILED(hResult));&&&&&& }&&&&&&&else&if(VertexShaderConstantPara.uiType&==&VSUserConstant::VT_FLOAT)&&&&&& {&&&&&&&&&&&hResult&=&m_pDevice-&SetVertexShaderConstantF(VertexShaderConstantPara.uiStartRegister,(constfloat&*)VertexShaderConstantPara.pDate,VertexShaderConstantPara.RegisterNum);&&&&&&&&&&&VSMAC_ASSERT(!FAILED(hResult));&&&&&& }&&&&&&&else&if(VertexShaderConstantPara.uiType&==&VSUserConstant::VT_INT)&&&&&& {&&&&&&&&&&&hResult&=&m_pDevice-&SetVertexShaderConstantI(VertexShaderConstantPara.uiStartRegister,(const&int*)VertexShaderConstantPara.pDate,VertexShaderConstantPara.RegisterNum);&&&&&&&&&&&VSMAC_ASSERT(!FAILED(hResult));&&&&&& }&&&&&&&else&&&&&& {&&&&&&&&&&&VSMAC_ASSERT(0);&&&&&& }&&& })&&&&&&&if(VertexShaderConstantPara.uiType&==&VSUserConstant::VT_BOOL)&&&&&& {&&&&&&&&&&&hResult&=&m_pDevice-&SetVertexShaderConstantB(VertexShaderConstantPara.uiStartRegister,(const&BOOL*)VertexShaderConstantPara.pDate,VertexShaderConstantPara.RegisterNum);&&&&&&&&&&&VSMAC_ASSERT(!FAILED(hResult));&&&&&& }&&&&&&&else&if(VertexShaderConstantPara.uiType&==&VSUserConstant::VT_FLOAT)&&&&&& {&&&&&&&&&&&hResult&=&m_pDevice-&SetVertexShaderConstantF(VertexShaderConstantPara.uiStartRegister,(constfloat&*)VertexShaderConstantPara.pDate,VertexShaderConstantPara.RegisterNum);&&&&&&&&&&&VSMAC_ASSERT(!FAILED(hResult));&&&&&& }&&&&&&&else&if(VertexShaderConstantPara.uiType&==&VSUserConstant::VT_INT)&&&&&& {&&&&&&&&&&&hResult&=&m_pDevice-&SetVertexShaderConstantI(VertexShaderConstantPara.uiStartRegister,(const&int*)VertexShaderConstantPara.pDate,VertexShaderConstantPara.RegisterNum);&&&&&&&&&&&VSMAC_ASSERT(!FAILED(hResult));&&&&&& }&&&&&&&else&&&&&& {&&&&&&&&&&&VSMAC_ASSERT(0);&&&&&& }&&&&ENQUEUE_UNIQUE_RENDER_COMMAND_END&&&&&&&VSMAC_ASSERT(!FAILED(hResult));&&&&return&!FAILED(hResult);&&&&&&&&return&1;}这个封装了D3D&设置vshader的函数,参数也是在buffer里面分配的,这里记录了,分配的地址和长度。&运行效果这里运行了2个&CLOD&地形,一个是用四叉树地形,一个是ROAM地形,还有个25个带骨骼的,带diffuse normal specular贴图的模型,用了双视口,加了一个黑白的后期处理效果。画骨骼模型是为了展示基本的low level render command&,四叉树地形用的是high level render command,ROAM地形用的是双D3D index buffer&切换的,画骨头的那个是用的我提到的dynamic mesh。把后期去掉把讨厌的画骨头去掉去掉后期来个线框模式专门看看四叉树地形lodROAM&地形lod切换个分辨率&
分类:(原创)渲染技术圈
登录后参与讨论。点击
请勿发表无意义的内容请勿发表重复内容请勿发表交易类内容禁止发表广告宣传贴请使用文明用语其它
淫秽色情政治倾向人身攻击抄袭剽窃广告刷屏恶意挖坟冒充他人其它}

我要回帖

更多关于 cocos2dx 多线程渲染 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信