全部新闻提供最新行业动态,分享前沿设计理念
3D渲染技术分享:3D游戏开发高手常用渲染调试技巧
时间:2024-06-04 来源:朝夕友人 点击:

零、本文主要知识点

友情劝退:全文7400+字

如果知识点里没有你想要的,那可以直接拉到底,与评论区大神一战。

最初的计划是想写一篇关于KylinsGraphicsDebugger实现原理的文章,但感觉那样的话,格局就小了。

正好最近麒麟子收到了许多研发负责人的反馈,说自己的技术团队开发效率一流,已经开发了不少2D/3D游戏。但遇上渲染优化、性能优化、内存优化、兼容性处理等项目后期问题的时候,就束手无策了。

特别是随着行业门槛越来越高,项目越做越大。从之前的2D到现在的3D,从之前的轻度到现在的中度、中重度,这个问题变得越发明显了。

这个事情让我思考了许久,这些经验丰富的技术团队到底哪方面的知识点有所欠缺呢?

经过多番沟通后发现,并不是他们不会处理,而是不知道从哪里下手。

大部分团队在和我讨论完项目情况后,根据我提供的思路都可以快速定位到问题,并迅速解决。

麒麟子前阵子写的KylinsGraphicsDebugger就是为了给这类团队提供一个便利的调试工具。

而此工具涉及到的内容可以说是3D项目开发中不可或缺的知识点,因此麒麟子~抖着胆~安排了如下内容:

0、新手和高手的差距在哪里

可以持续升阶的技能定位问题 vs. 解决问题调试技巧与方法的重要性1、Mipmaps查看器

3D建模小姐姐和程序员李二狗的故事如何使用Mipmaps查看器美术标准-模型纹理大小制定显存优化-纹理分辨率合理性检查Mipmaps查看器实现原理2、LightmapUV查看器

什么是Lightmap如何使用LightmapUV查看器LightmapUV查看器实现原理3、材质实例查看器

常见DrawCalls优化方法DrawCall合并失效排查材质实例查看器的使用与实现原理4、Overdraw查看器

什么是Overdraw如何使用Overdraw查看器为什么减少Overdraw可以提升性能常见的Overdraw优化方法Overdraw查看器实现原理

一、天、地、玄、黄

麒麟子记得,在某部修真小说里面,功法被分为了 天、地、玄、黄 四阶。 其中天阶功法最高,黄阶功法最低。从设定上而言,玄阶功法中级也可以秒黄阶功法满级,功法越高阶,差距会更大,以此类推。

但也有例外,有一些黄阶功法,会随着使用者的修练程度而升阶,功法大乘可达天阶。就是说,如果有幸得到一门这样的功法,使劲练就可以了,不用担心投入在他身上的时间过多,最后被掌握的天阶功法取代。

修真小说主人公就是靠着他老师给的一个开了挂似的技能,从啥也不是到最强王者,最终迎娶多个白富美,走上人生巅峰的。

虽然主人公后期修习了不少高阶功法,但大部分是以这个开挂技能作为催动的。

二、开发与调试

不知道大家有没有听过下面这个故事。

某个工厂一台机器出了问题,大家都不知道问题出在哪里。无奈之下从国外请来了一名专家,专家观察一阵后在机器壳上画了一条线,并让相关人员按画线的地方进行检查。相关人员很快就找到了问题所在并轻松解决掉。

工厂老板支付专家10000美元,专家拒绝了,说我只是画了一条线而已。 老板说:画线只值1美元,知道在哪里画线,值9999美元。

这个故事告诉我们,有时候解决问题非常容易,但定位问题原因却很难。

有一定项目经验的小伙伴应该都清楚,在极端情况下,我们用于验证自己所写代码正确性、稳定性、兼容性、运行性能等方面的时间,远远大于编码时间。我们把这个验证过程称为调试(Debugging),就是找出问题并解决。

一个开发者从新手到高手的上升过程,粗略地说就是写BUG和改BUG的过程。

而调试这个技能,区分高手和新手的关键之处,就在于定位问题。 使用同样的调试工具、同样的调试方法,高手可以快速定位问题原因,而新手往往无所适从。

某些问题的解决往往只需要修改一个符号,但却因为找不到问题所在而无法修改。

对于编程而言,你可能会学不同的平台,不同的框架,不同的语言。但调试的工具、调试的方法却总是大同小异的。

所以,花时间去掌握调试技巧与方法,一定是投入产出比最高的部分。

三、扯这么多干嘛?

文章的一、二两节,麒麟子扯了两个八杆子打不着的话题。但若你仔细体会,定能发现。麒麟子做了这么长的铺垫,就是想告诉大家调试的重要性与投入产出比。

调式技巧与方法在软件开发的领域,就好比那修真小说里,一个可以从黄阶升到天阶的功法。但由于起步比较低阶,新手也能修练,从而往往被人忽略。

所以,本文最终的定位为:3D项目开发中的常见问题,以及如何利用KylinsGraphicsDebugger调试这些问题。

同时,为了照顾一些想要知道工具是如何实现的朋友,在每一个工具末尾,也会讲到此工具的实现方法。

从麒麟子从业经验来看,这些是必须掌握的。会让你的日常3D开发相关工作事半功倍。

四、Mipmaps查看器

4.1、美术小姐姐和程序李二狗的故事

程序员甲已经入行两年多了,经过自己不懈的努力,已经能够熟练调用各类3D游戏开发中的常用API了。凭借着不错的机缘,参与了好几个3D游戏的开发。靠着自己过人的本领,深受美术小姐姐们的喜爱,小到软件的安装与卸载,大到系统的维修与重装,都找他。

有一天,做模型的3D美术小姐姐跑过来问程序员甲:程序哥哥,你看我刚刚发你的截图,我这个模型用多大的贴图适合啊?

这是第一次在专业领域回答小姐姐的问题,程序员甲觉得是时候展现自己真正的实力了。一定要借这个机会好好表现一番,要将毕生所学,融入答案之中,试图让妹子知道他那无与伦比的技术造诣,最好能产生出一些崇拜心理。

于是程序员甲回答到:“在保证显示效果达标的情况下,越小越好,因为大了会增加包体大小以及带来更多的内存使用。”

小姐姐听完答案,回了他一个迷人的微笑,然后转身朝自己的工位走去。

突然,旁边的李二狗大吼一声:“美女,请留步!”,见小姐姐停下脚步,李二狗继续说道:“以我们现在的摄像机距离,128x128就行。但策划好像想把摄像机拉得更近一些,为了保险起见,你先用256x256吧。”

小姐姐转身笑着对李二狗连声说谢谢,依然是那迷人的微笑,但感觉似乎与之前对着程序员甲的微笑又有所不同。

这时候程序员甲不乐意了:李二狗说是多少就是多少,凭什么?

李二狗:“就凭我前几天用Mipmaps查看器分析过。“ ....

故事就不继续编了,最后李二狗教会了小姐姐用查看器,但小姐姐也没有和李二狗共进晚餐。 因为,使用Mipmaps查看器完成这项工作真的是太简单了(就当是这个原因吧)。

4.2、如何使用Mipmaps查看器

如上图所示,当我们开启Mipmaps查看模式的时候,场景就会被渲染为不同的颜色。

右下角的色卡,是颜色与尺寸的对应关系。

例1:近处的红色,就表示它需要2048x2048的纹理。

例2:士兵身上,以绿色,橙色,黄色面积最大,因此,士兵的纹理应该不超过512x512大小,超过就是浪费。

例3:远处的树出现了紫色,绿色,表示1024x1024的纹理就够用。

注:通常近处的地面和远处的树,是用不到2048x2048,1024x1024这么大的纹理的。出现这个原因是因为地面和树用的是普通的贴片,而非3D建模。

有了上面的知识,回答故事中美术小姐姐的问题,只需要将摄像机调整到最常用的视角,然后看看角色在这个模式下的颜色就行了。

你,学,会,了,吗?

假如你的项目已经快要上线了,此时发现显存(手机上只有内存)占用严重超标,怎么办呢?

由于纹理是显存资源消耗的大户,我们如果能减少纹理的显存占用,可比什么都要强。

通过常见的动态加载卸载纹理资源管理方案,可以有效的使得不使用的纹理不再占用显存,降低内存峰值。

但假如场景同时使用的纹理就需要这么多,怎么办呢?

我们还是可以利用Mipmaps查看模式来做优化。

首先我们给场景中的资源编排一个重要性,分类检查是否可以降低纹理分辨率。

每降低一个Mipmap层级,可以节约75%的显存占用。

还是用上面的图来举例。

我们认为细节重要程度为 背景<地表<角色。所以我们检查的顺序也是按背景、地表、角色这样来。我们会优先找出这些地方偏紫``偏红的部分,再做优化,因为尺寸越大的纹理优化的收益越高。

我们看到远处的树大面积为绿色,少部分为紫色。那么我们完全可以将远处的使用的所有纹理控制在512x512以内,通过UV调节,UV重叠等优化手法,甚至可以下降到更低。

而地表的红色,表示需要2048x2048才能够胜任。

如果降低为1024x1024是可以接受的,我们可以直接降低到1024x1024。如果降低到1024x1024无法接受,我们就需要考虑UV复用的优化了。 通常调节UV,或者采用多重纹理混合+Texture Tiling来解决,既能保证效果,又不损失画面细节。

可能几张128x128就可以做出非常完美的效果了。

看到了吧,就这么简单的东西,能够派上这么大的用场。

4.3、Mipmaps查看器实现原理

跳过本小节并不影响Mipmaps查看器的使用

要了解这个问题,我们得先从Mipmap的原理说起。

Mipmap的定义非常多,一个普遍的定义是:

Mipmap是通过预先生成一系列的低通滤波图像,来避免采样频率低和数据频率高的情况下出现摩尔纹导致画面失真的问题。

至于为什么会形成摩尔纹,则需要对纹理采样的实现细节进行深入学习。

感兴趣的朋友可以搜索:Mipmap详解这个关键字。

所谓Mipmap的一系列低通滤波图像的具体做法,就是通过一些过滤算法预先生成不同尺寸的图像。

比如一个512x512的纹理,它的mipmap levels大小如下:

0: 512x5121: 256x2562: 128x1283: 64x644: 32x325: 16x166: 8x87: 4x48: 2x29: 1x1

那么问题就来了,GPU是通过什么方式决定使用哪一级的mipmap的呢?

GPU在执行纹理采样的时候,会根据相邻纹理坐标的缓冲区像素跨度来计算需要用到哪一个层级。

这就是说,采用哪一级的mipmap与模型的缩放平移旋转以及大小无关,只与最终它出现在画面上的尺寸有关。

通常情况下,各级的mipmap是根据上一级的mipmap,通过适合的算法生成下一级的图像。

但是,我们也可以手工指定每一级的纹理内容,可以让每一级的内容都不一样。

假如有一张特殊的纹理,它不同的mipmap是不同的颜色,渲染的时候会出现什么效果呢?

基于上面的知识内容,我们很容易想象得到,得到的图像就是本节开头的截图。

而由于不同的颜色对应了不同的尺寸,我们就可以通过颜色对应关系,找到适合的纹理大小了。

关键代码如下:

//定义不同的MIPMAP等级颜色。 public get mipmapsInfo() { if (!this._mipmapsInfo) { this._mipmapsInfo = [ { s: 2048, r: 255, g: 0, b: 0 }, //红 { s: 1024, r: 139, g: 0, b: 255 }, //紫 { s: 512, r: 0, g: 255, b: 0 }, //绿 { s: 256, r: 255, g: 165, b: 0 }, //橙 { s: 128, r: 255, g: 255, b: 0 }, //黄 { s: 64, r: 0, g: 0, b: 255 }, //蓝 { s: 32, r: 255, g: 255, b: 255 }, //白 { s: 16, r: 128, g: 128, b: 128 }, //灰 { s: 8, r: 128, g: 128, b: 128 }, //灰 { s: 4, r: 128, g: 128, b: 128 }, //灰 { s: 2, r: 128, g: 128, b: 128 }, //灰 { s: 1, r: 128, g: 128, b: 128 }, //灰 ]; } return this._mipmapsInfo; } //通过生成一系列的ImageAsset并赋值给Texture2D:mipmaps private generateSpicialMipmapsTexture(): Texture2D { let mipmaps = Array<ImageAsset>(this.mipmapsInfo.length); let pixelBytes = 4; let pixelFormat = Texture2D.PixelFormat.RGBA8888; for (let i = 0; i < this.mipmapsInfo.length; ++i) { let info = this.mipmapsInfo[i]; let width = info.s; let height = info.s; let arrayBuffer = new Uint8Array(width * height * pixelBytes); for (let i = 0; i < arrayBuffer.byteLength / 4; ++i) { arrayBuffer[i * pixelBytes + 0] = info.r; arrayBuffer[i * pixelBytes + 1] = info.g; arrayBuffer[i * pixelBytes + 2] = info.b; arrayBuffer[i * pixelBytes + 3] = 255; } let imageAsset = new ImageAsset({ width: width, height: height, format: pixelFormat, _data: arrayBuffer, _compressed: false }); mipmaps[i] = imageAsset; } let texture = new Texture2D(); texture.mipmaps = mipmaps; texture.setMipFilter(Texture2D.Filter.LINEAR); texture.setFilters(Texture2D.Filter.LINEAR, Texture2D.Filter.LINEAR); return texture; }

五、LightmapUV查看器

本来给大家准备了一个感人的故事,还是关于李二狗的。但由于篇幅原因,就此省略,我们直接进入主题。

5.1 什么是光照贴图

在项目中,有时候我们需要追求较高的阴影效果的同时又保持较好的渲染性能。这种情况下,采用预烘焙光照贴图(Lightmap)来实现阴影是较好的方式。

光照贴图通常采用第二套纹理坐标进行采样,但模型师有时候会忘记,导致负责在场景编辑时光照烘焙效果不正确。

而此时负责烘焙的人并不确定是自己参数没设置问题,还是模型本身的UV就有问题。

于是这个LightmapUV查看器就是用于查看模型第二套纹理坐标的。

5.2 如何使用LightmapUV查看器

看到上面这个图了没?

当我们进入LightmapUV显示模式的时候,如果一个模型具备用于Lightmap的第二套模型UV,则会显示出红黄绿颜色(为什么显示出这个颜色,下面会讲)。

如果一个模型没有第二套UV,则为黑色。

这个功能非常简单实用,可以帮助烘焙师快速排查问题原因。

5.3 LightmapUV查看器实现原理

这个的实现超级简单,将模型的第二套纹理坐标用颜色的方式输出即可。

下面是对应的Shader代码:

vec4 frag () { vec4 col = vec4(v_uv1.x,v_uv1.y,0.0,0.0); return CCFragOutput(col); }

由于我们在输出uv的时候只做了r,g通道映射,所以不会有蓝色通道出现,最终会呈现出红黄蓝系列的颜色。

六、材质实例查看器

6.1 减少DrawCalls的常见方法

不管是2D游戏还是3D游戏,在做性能优化的时候,减少DrawCalls的操作都是必不可少的。

减少DrawCalls最常见的三种手法为:

1、剔除(Culling)2、静态合批(Static Batching)3、动态合批(Dynamic Batching)3、GPU几何体实例化(GPU Instancing)

这不是一篇专门讲减少DrawCalls的文章,但都提到它了,麒麟子非常想友情提示一下这三种手法的优缺点。

剔除:会增加CPU运算负担,但是它不仅可以减少DrawCalls,还能降低GPU负担。

因此,只要剔除算法得当,它总是会被优先选择。

引擎渲染管线一般会内置剔除算法,但对于一些特定场合的游戏,我们可以通过自定义更精准、更简便的算法来将剔除收益最大化。

静态合批:静态合批只会在合并时增加CPU负担,因为会在CPU上做Mesh合并。

但由于只做一次,因此可以用一帧的CPU负担换来无数帧的DrawCalls减少, 是非常划算的。

动态合批:动态合批会增加CPU负担,由于CPU侧每帧(或者标记为需要重新合并时)都会进行Mesh合并,所以不适合合并的顶点数过多的情况,否则可能得不偿失。

不要强行减少DrawCalls哟

GPU几何体实例化:几何体实例化需要参与的对象完全是一致的(Mesh和Material都得相同),同时还需要获得硬件和平台系统的支持。 好在目前的普及率是非常高的,不必担心兼容性问题。

6.2 材质实例查看器的使用与实现原理

上面讲到的合批和几何体实例化,生效的前提是材质(Material)相同。

麒麟子在开发项目的过程中,曾经遇上过一个特别奇怪的问题:怪物实例化效果一开始是好的,但玩着玩着就失效了。

RenderDoc,Spector.JS,XCode都用上了,也没有找到失效的原因。怪物太多了,并不好抓取。

既然如此,就得自己想办法了。

实例化失效只有两种可能:1、模型变了 2、材质变了。

模型是不可能变的,这辈子都不可能变的,因为根本没有改它,那只能是材质变了。

于是我写了一个特殊的材质调试器:

1、统计场景中使用到的材质,拿到其实例ID2、给每一个实例ID分配一个随机的颜色,并创建一个纯色的材质3、将实例ID对应的材质,替换为这个纯色的材质

这样做了以后,可以实现的结果就是:不同材质实例的对象,会显示成不同的颜色。

如下图所示:

写好这个调试器之后,当发现实例化功能失效时,开启这个调试器,发现怪物都变成了不同的颜色,而变成不同颜色的怪物恰好是被攻击过的怪物。

有了这个线索我们顺藤摸瓜,找到怪物被攻击的函数,锁定了受击变色这个功能。

发现在操作怪物材质的时候,用了.material.setProperty,而这里是不能使用.sharedMaterial.setProperty的,因为只想要对被攻击的怪物变色。

注:首次调用MeshRenderer的getter material,会触发材质clone。

注:调用MeshRenderer的getter sharedMaterial,不会触发材质clone,但会影响所有使用这个材质的对象。

使用材质实例查看器快速定位了问题原因,最终简单地做了一个受击对象材质池,通过材质替换和复原,解决了这个问题。

更多应用场合...

除了排查程序动态修改出现的材质问题,也可以检查场景原本不合理的材质实例引用问题。

比如,在进行场景编辑的时候,原本两个外观一致的对象,不小心建立了两个材质实例文件,看起来是一致的,但会导致合批和GPU几何体实例化方案失效。

七、Overdraw查看器

7.1 什么是Overdraw

Overdraw是指渲染帧中像素重绘的次数。

我们最理想的状态,就是屏幕上的每一个像素位置,只绘制了一次。

当我们仅在屏幕上显示一张图片的时候,由于只画了一次Quad,没有造成额外的遮挡。这种情况下,每个像素位置的像素就只绘制了一次,我们认为Overdraw为0。

我们在这张图片上又绘制了一张稍小一点的图片,那这张稍小一点的图片与大图重合的部分,Overdraw为1。

3D世界的渲染则更为复杂,产生的Overdraw就更多了。

当Overdraw的值超过一定数量,就会导致性能问题。比如大面积出现Overdraw超过10的像素区域。

7.2 如何使用Overdraw查看器

Overdraw的查看也是非常简单的,常见的经典模式如下图所示:

当我们开启Overdraw查看功能时,整个场景会被渲染成红色。其中,越亮的地方表示Overdraw次数越多。

大家有没有发现一个问题,通过这张图,如何找出Overdraw大于5或者10的呢?

麒麟子在研究的时候,也发现了这个问题。要知道,每个人的色觉和色温的敏感度是不一样的。单纯通过颜色深浅、明暗来判断值是不科学的。

为了让结果更加直观,麒麟子用色卡的方式做了一个数据可视化,如下图所示:

通过图中的颜色与色卡对比可以发现,这个场景中,Overdraw>=10的只有树根下面的一点点(只需要找到红色区域即可)。Overdraw>=5的集中在角色身上,地上的草,以及左上角的树冠。

有了这个直观的查看方式,剩下的就是针对具体问题进行优化了。

7.3 为什么减少Overdraw可以提升性能

一个模型要呈现到屏幕上,主要会经历:顶点处理->光栅化->像素处理->深度测试->模板测试->混合(半透明)->写入缓冲区等步骤。

而这个流程里面的像素处理对GPU造成的运算负担和纹理填充率负担较大。

流程里的混合与写入缓冲区对GPU造成的像素填充率负担较大。

对于非透明对象,由于不需要混合,同时不管从硬件还是渲染管线都有许多针对非透明对象的深度测试优化。

许多非透明对象产生的无效像素从像素处理阶段一开始(TBR架构甚至在此之前)就被丢弃了。

这样使得非透明对象渲染时不对会缓冲区造成太多不必要的读写操作,因此我们暂时可以不用理会非透明对象造成的Overdraw压力。

但是对于半透明对象,情况就不容乐观了。半透明对象会开启混合,同时关闭深度写入。

就是说,半透明对象之间不会出现因为相互遮挡而产生深度剔除,所有的半透明对象有很大概率会完整的走完全部流程。

混合操作会先读取目标缓冲区内容,与当前缓冲区内容做混合操作,再将结果写入目标缓冲区。而其中大量的混合操作会对GPU的像素填充率产生较大压力。

Overdraw真正让人无法接受的,不是开销,而是划不来。

Overdraw最大的划不来在于,一些看不见的像素被渲染了,有两种情况:

1、先写入的像素后来被覆盖了(非透明物体)2、写入的是ALPHA=0的像素(透明物体)想像一下,一个粒子系统,使用一张512x512的纹理,纹理只有正中央一小部分有内容,其余内容全是透明的。这样导致的Overdraw问题就非常划不来了。

7.4 常见的Overdraw优化方法

硬件架构

TBR或者TBDR硬件架构会将深度测试提前到像素处理之前,所有开启了深度测试的渲染对象都能够受益。这对所有的非透明物体以及与被非透明物体遮挡的透明物体有效。

Pre-ZPass

一些渲染流水线,或者硬件(针对PC平台居多),会内置Pre-ZPass功能。就是在正式渲染场景之前,先用一个深度输出Shader渲染所有要写深度的物体到一张渲染纹理上。

接下来,在正式绘制物体的时候,在像素Shader中会先做一次深度比较,深度比较失败的像素将直接被丢弃,以省略掉后面所有的像素处理、混合以及像素写入缓冲区过程。

非透明物体从近到远渲染

大家都知道,透明物体是由远及近的顺序渲染,以确保混合效果的正确性。非透明物体由于有深度测试的存在,是不需要做这个事情的。

然而为了优化Overdraw,许多引擎会选择非透明物体从近到远渲染。因为越近的物体,越有可能遮住较远的物体。

还有一个特别的渲染就是天空盒,由于天空盒会占满整个屏幕。如果它先渲染的话,相当于全屏Overdraw+1,所以通常情况下,我们的天空盒是放在非透明渲染之后,透明渲染之前的。

3D图形引擎中常见的渲染顺序:

非透明2D UI -> 非透明3D对象 -> 天空盒 -> Alpha Test对象 -> Alpha Blend对象 -> 透明2D UI

Trim Image

Cocos Creator引擎提供了Trim图片的功能(仅支持Sprite),此功能开启时将会裁剪掉图片中完全透明的边缘部分。

这个功能可以减少许多UI上的Overdraw。

更优的办法,还是建议美术出图时不要留太多不必要的空白

采用Mesh做出轮廓

如图所示,左边是普通的Sprite渲染(红色是代表纯透明部分),右边是用一个Mesh去逼近图片内容的轮廓。这样可以大大减少Overdraw的浪费,从而提升性能。

采用模型UV动画替代粒子效果 粒子系统为了保证效果,通常会让粒子达到一定的数量。

但粒子之间相互重叠,且粒子纹理本身会有许多的纯透明浪费。

对于一些如火盆中的火焰,物体的发光。

若这类能够使用UV动画来替代则能带来一定的Overdraw提升。

以上就是常见的降低Overdraw的手法,我们操作空间其实不多。总结一下:

1、尽量减少场景中的半透明对象。2、半透明对象所用的纹理尽量不要有不必要的空白。3、用Mesh做出轮廓,减少对图片空白区域的使用。4、在能够满足需求的情况下,少用重叠较多的渲染方式,比如:粒子。

7.5 Overdraw查看器实现原理

红色经典模式的Overdraw显示非常简单:

1、编写一个特殊的材质2、替换掉需要查看的对象的材质

材质的Shader出奇的简单,每绘制一次,红色通道+0.1。

vec4 frag () { vec4 col = vec4(0.1,0.0,0.0,1.0); return CCFragOutput(col); }

色卡模式的Overdraw显示,是在红色经典模式的基础上,将结果输出到一张RenderTexture,再用另一个Shader对其进行采样。

由于每绘制一次是+0.1,因此红通道的值乘以0.1就可以计算出绘制了多少次。

Shader代码如下:

vec4 frag_display(){ vec4 color = texture(mainTexture,v_uv); int index = int(floor(color.r * 10.0)); if(index > 10){ index = 10; } vec4 o; for(int i = 0; i < COLOR_TABLE_LEN; ++i){ if(i == index){ o = colorTable[i]; break; } } return CCFragOutput(o); }

八、结束语

由于本文内容较多,信息量较大,如果很忙的话,建议先收藏,再找个适合的时间阅读。

对于急着想使用的朋友,可以忽略实现细节,直接集成KylinsGraphicsDebugger到项目中即可。

对于想要学习其中细节并充分理解,甚至想自己实现类似工具的朋友,也推荐先使用相关工具,有一定理解之后,再上手自己实现会容易得多。

最后,感谢大家百忙之中陪麒麟子一起学习。

欢迎大家通过公众号里的联系方式添加我的微信好友。

如果觉得本文内容不错,请点赞并关注麒麟子,麒麟子会继续带来更多内容。

想要 KylinsGraphicsDebugger 的朋友可以知乎私信麒麟子。

如果您也有此需求,欢迎咨询我们立即咨询