对于游戏开发者来说,在开发过程中,加入 UI 的支持是不可或缺的一环,不过想要自己动手敲代码实现 UI 实属一件难事,后来 ImGUI
诞生为开发者们带来直接拿来用般的便利,而这是否意味着 ImGUI 即将去掉传统的 GUI 设计?
作者 | nil
译者 | 苏本如,责编 | 屠敏
出品 | CSDN(ID:CSDNnews)
一个即兴的、未经深思熟虑的想法是:像Dear ImGUI这样的东西有可能是主流UI库的未来吗?
对于那些不知道什么是即时模式图形用户界面或ImGUI的人,可以看看Casey
Muratori在2005年制作的一个还算有名的视频:https://youtu.be/Z1qyvQsjK5Y(需科学上网)。
大多数使用ImGUI风格的程序员发现,使用ImGUI来创建用户界面比使用传统的保留模式图形用户界面(GUI)要容易得多。而且性能会得到显著地提高。
典型的保留模式,面向对象的GUI框架是一个系统。在该系统中,你基本上创建了一个GUI框架控件(widget)的“场景图”(窗口、网格、滑块、按钮、复选框等等)。你将你的数据复制到这些控件中,等待事件(event)或回调(callback)在控件被编辑时接收到通知。然后查询控件的新值并将其复制回你的数据中。
这种模式几乎应用于所有的GUI系统中。Windows、WFP、HTML DOM、Apple
UIKit、Qt,你能叫出名字的99%的GUI框架都属于保留模式的,面向对象的,“场景图”式的GUI。
这种模式的GUI存在的问题是:
*
必须编写大量代码来管理GUI对象的创建和销毁。
设想你有一个滚动列表,你经常需要创建100多个或1000多个GUI控件(就像HTML,创建一个TR,然后是TD,然后是每个TD的内容,等等)。如果数据真的很大,你最终不得不创建一些控件的虚拟窗口,要么在用户滚动时创建新的窗口并且删除旧窗口,要么将旧窗口从后面拉出来,然后将其添加到前面。其结果是:你需要写的代码太多了。
*
创建和销毁对象导致UI反应迟缓。
由于GUI对象的创建和销毁速度很慢(通常它们是非常大的对象),因此通常需要编写大量的代码来帮助寻找和设计解决方案,以最小化需要创建和销毁的对象数量。
想想React如何使用虚拟DOM来识别差异,然后将这些差异应用到实际的GUI控件和DOM树/场景图中。
*
你必须编制数据传入/传出控件。
这就需要先将数据复制到控件中,然后对事件做出响应,并将控件中的新数据读回。需要编写更多的代码。
与此相反,ImGUI中没有对象,也几乎没有状态。大多数ImGUI的简单做法是像下面这样调用函数:
// draw a buttonif (ImGUI::Button("Click Me")) { IWasClickedSoDoSomething();}// draw sliderImGUI::SliderFloat("Speed:" &someInstance.speed, 0.0f, 100.0f);
if (ImGUI::Button("Click Me")) { IWasClickedSoDoSomething(); } // draw slider
ImGUI::SliderFloat("Speed:" &someInstance.speed, 0.0f, 100.0f);
这里的Button和Slider做了两件事:
*
它们将绘制控件所需的位置和纹理坐标添加到一个向量(数组)中。如果控件被裁剪出屏幕或在当前窗口/裁剪矩形之外,则坐标不被添加。
*
它们检查鼠标指针的位置、键盘状态等,以操作该控件。如果数据发生变化,它们会立即返回。
所以,这样做有如下优点:
*
丝毫不需要分配内存,也即需要的内存为零!
*
速度很快。即使使用非常复杂的UI并且只有单线程的情况下,大多数(如果不是全部)ImGUI在60fps(帧)的速度下运行没有任何问题。
*
不需要对必须管理的对象进行创建和销毁操作。
*
没有状态,因为没有对象来存储状态。
*
基本不需要编制数据。
*
没有需要注册或响应的事件或回调。
下面两点可能是这样做的缺点:
*
可能需要更多的CPU。
我还不能确信这一点总是对的。保留模式GUI设计的初衷是为了尽量减少工作量。假设你有一个类似微软Excel的用户界面。它有75个工具栏按钮和显示300个单元格的电子表格。输入光标位于单元格E7中,并且在闪烁。如果回到Windows
3.0(及更早版本),CPU将绘制像素(GPU那时不存在)。GUI系统确定只需要重新绘制光标本身大小的一些小区域,并且只需要将这些像素直接重新绘制到屏幕内存中。同样,如果键入字母,系统只能确定单元格E7已被修改,只需重新绘制单元格E7。
在1993-1994年的计算机上,这点很重要。因为计算机无法以每秒60帧的速度绘制整个屏幕。
因此,对于传统的基于“场景图”的面向对象的保留模式GUI来说,这是最好的做法。
需要注意的是,系统仍然需要检查图形用户界面的大部分地方来计算最小的影响区域是什么。这可能不如重新绘制每个像素的工作量大,但需要的工作量也不少。
ImGUI则相反,任何时候你想更改任何内容,整个图形用户界面就要重新绘制。即使是光标。以我们进入Excel示例,所有75个工具栏控件和300个单元格都将因为一个闪烁的光标而重新绘制。
这是ImGUI的最坏情况。大量的CPU被浪费了。
再拿滚动电子表格作个对比。
在基于场景图的保留模式的图形用户界面中,假设您按下page
down键,很可能300个单元格控件会被删除,300个新的单元格控件会被创建,每个单元格的数据将被复制到每个单元格控件中。从所有这些来看,GUI系统将遍历所有300个单元并将它们绘制出来。
相反,在ImGUI的情况下,不会删除任何旧控件,不会创建任何新控件,也不复制任何数据,
300个单元格要像先前一样绘制出来。在这种情况下,ImGUI为更新整个显示页面所需要的CPU工作量仅仅是保留模式GUI系统工作量的十分之一至百分之一。
哪种情况更常见呢?对于一个文本编辑器来说,通常只有很小的变化,所以场景图式的GUI会获胜。但是对于Instagram或Facebook应用程序,人们几乎经常滚动页面,在这种情况下,ImGUI以压倒性优势获胜。
*
可访问性问题
使用保留模式GUI,所有控件的数据都已复制到GUI的场景图中。这意味着GUI系统本身可以查看这些数据并提供不同的接口(比如放大,说出它,变成盲文,等等)。
而使用ImGUI的情况下,通常GUI不保留任何数据,所以它可能做不了保留模式GUI能够做的那些事情。
这可能是一个值得研究的地方。可能存在一些方案可以使ImGUI能够比传统方法更好地处理可访问性问题。大多数ImGUI用于游戏开发,它针对的对象是同一团队中的游戏开发人员,而不是最终用户。也就是说,没有动力去推动对这些解决方案的探索。
下面两点感觉是缺点,但可能不是:
*
样式
我不太确定大多数保留模式GUI是如何支持换肤的。可能是使用最具样式风格的,且包含了所有1000多个CSS选项的HTML DOM。
对于ImGUI来说,样式是由你来设计的。添加更多的样式选项,甚至是几乎所有的CSS或者至少是好的那部分CSS,可能是相对容易实现的,而且能保持好的性能。更好的地方在于:你可以很容易选择需要这些样式或者不需要这些样式。所以如果你的应用程序不需要这样的样式,为什么要浪费内存或CPU时间来处理它呢?为什么要像大多数保留模式GUI那样,不管你使用如否,都要将所有的样式数据嵌入到每个控件中呢?考虑一下HTML,如果每个元素都有100个样式设置(毫不夸张确实有100个设置),那是一件多么可怕的事。
*
动画
大多数ImGUI都是无状态的,所以所有的动画都取决于应用程序。虽然很容易想到使用存有少量动画状态数据的包装器(wrapper)可以很容易地将UI动画放回。但是事实上,包装器可以让你选择只在重要的地方支持动画,比如样式。大多数保留模式的GUI都保存有大量的数据、状态和每个控件的设置,无论你使用如否。
对这点我真的很好奇。我知道大多数GUI框架作者都怀疑ImGUI是一个好的模式。而且据我所知,没有人真的尝试过它。如前所述,大多数的ImGUI用于游戏开发。而要想找到合适的模式来完全复制像苹果的UIKit这样的精致奢华的东西,需要各方的共同努力。这能做到并保持好的性能吗?把所有功能/特性添加回去会让它失去性能优势吗?ImGUI的基本设计是否意味着它最终将保持性能和易用性?如果没有场景图式的GUI,我们是否会发现某些功能/特性不可能真正地实现?
我还要补充一点,在某种程度上,React在使用上类似于ImGUI。React有JSX,但它只是函数调用的简化。最大的区别在于:
*
不需要渲染器,因为每个组件都会立即渲染。。
*
不需要隐藏的虚拟DOM。
*
不需要设置状态,因为它是无状态的。
*
不需要附加事件或处理像componentWillMount (组件将要加载), componentDidMount(组件加载),
componentWillUnmount(组件将卸载)等事件,因为没有组件,只有函数,也没有控件(DOM元素、本地对象等)。
如果我们把上面的代码翻译成假想的ImReact语言,它看上去可能像下面这样:
const Button = (props) => { return ImGUI:Button(props.caption);};const SliderFloat = (props) => { return ImGUI:SliderFloat(props.caption, props.value, props.min, props.max);};const Form = (props) => { if (<Button caption="Click Me">) { DoSomething(); } <SliderFloat min="0" max="100" value="&props.speed" caption="Speed:" />};
return ImGUI:Button(props.caption); }; const SliderFloat = (props) => {
return ImGUI:SliderFloat(props.caption, props.value, props.min, props.max); };
const Form = (props) => { if (<Button caption="Click Me">) {
DoSomething(); } <SliderFloat min="0" max="100" value="&props.speed"
caption="Speed:" /> };
只要看一下React的代码,你会发现把它转换成真实的代码是非常简单的。
我不是非常确定对speed参数的更新是如何工作的,但我猜是将C++(ImGUI)和JavaScript(React)混合一起的作用。典型的ImGUI要么具有一种Javascript所不具有的模式,它能够传递进来一个指向原始值的指针,要么返回新值(代码如下):
newValue = ImGUI::SliderFloat(caption, currentValue, min, max);
如果你想使用和你编写的Dear IMGUI C++示例相同的用法,那么代码如下:
someInstance.speed = ImGUI::SliderFloat("Speed:", someInstance.speed, 0.0f, 100.0f);
0.0f, 100.0f);
所以,如果我们假设了API的样式,那么代码可能是这样的:
const Button = (props) => { return ImGUI:Button(props.caption);};const SliderFloat = (props) => { return ImGUI:SliderFloat(props.caption, props.value, props.min, props.max);};const Form = (props) => { if (<Button caption="Click Me">) { DoSomething(); } props.speed = (<SliderFloat min="0" max="100" value="{props.speed}" caption="Speed:" />);};
return ImGUI:Button(props.caption); }; const SliderFloat = (props) => {
return ImGUI:SliderFloat(props.caption, props.value, props.min, props.max); };
const Form = (props) => { if (<Button caption="Click Me">) {
DoSomething(); } props.speed = (<SliderFloat min="0" max="100" value=
"{props.speed}" caption="Speed:" />); };
注意到上面的组件没有返回虚拟DOM节点,因为不需要。我们唯一真正要做的是JSX,它只是为了表明,如果你愿意,你可以使用React样式模式。
注意:在这个例子中,不要陷入直接的状态操作中。如何更新状态不应由UI库决定。不管你使用哪个GUI系统,你都可以自由地管理状态。这个例子显示了ImGUI样式是多么地简单。
state.value = ImGUI:SliderFloat(caption, value, min, max);value, min, max);
肯定比下面的保留模式GUI的实现要简单得多。
// at init timeconst slider = new SliderWidget(caption, state.value, min, max);slider.onChange = function(newValue) { state.value = newValue;}// if state.value changed slider needs to show the new valuefunction updateSlider(newValue) { state.value = newValue;}
const slider = new SliderWidget(caption, state.value, min, max);
slider.onChange = function(newValue) { state.value = newValue; }
// if state.value changed slider needs to show the new value function
updateSlider(newValue) { state.value = newValue; }
更糟糕的是,现在你需要以某种方式调用updateSlider函数,或者在每一处state.value被更新的地方调用它,或者你编写一个复杂的系统,以便所有需要更新state.value的地方都调用一个跟踪所有控件及其状态的函数。
ImGUI库不需要如此复杂。它不需要处理控件。无论状态中的值是什么,每个帧都是控件的内容。这与React的承诺相同。但是,React使用的保留模式GUI库性能很差,它最终被这一点所拖累。
作为ImGUI可以实现复杂UI的一个例子,就是下面这个内容丰富的Unity编辑器界面。
这个例子表明,ImGUI在面向用户的应用程序中的使用也可以被优先考虑,而不仅仅只用在游戏应用中,即便Unity本身就是一个游戏制作软件。
这里的Readme文件(https://github.com/ocornut/imgui#gallery)中也包含各式各样的使用ImGUI制作的UI截图。
这里是Dear
ImGUI库中包含的示例的在线版本:https://greggman.github.io/doodles/glfw-imgui/out/glfw-imgui.html。
如果你想试着玩一下这个示例,请注意,它实际上不是为浏览器设计的,因此存在一些需要修复的问题。这些问题很容易解决,所以不要陷入吹毛求疵的小问题中。相反,你应该注意到它的UI非常复杂,但它能够以60帧的高速运行。使用主窗口中的“示例”菜单并打开更多窗口。展开主窗口中的示例,查看各种活动和复杂的控件。现在假想一下你试图使用HTML/DOM/React来制作同样复杂的UI。你会发现,不仅HTML/DOM版本会有很多卡顿,可能不可以60帧速度运行,而且实际实现的代码可能是多个维度代码的5到10倍。一个维度是使用HTML/DOM和/或React(vs.
ImGUI)实现UI需要编写的代码量。另一个维度是要在屏幕上获得UI需要执行的代码量。我怀疑在HTML/DOM版本中执行的CPU指令量比ImGUI版本高出100倍。
比较一下ImGUI::Button函数和生成<button>元素。
对于<button>元素来说:
1. 需要创建HTMLButtonElement对象
这个对象具有下面所有这些需要设置为某种值的属性
autofocus: boolean disabled: boolean form: object formAction: string formEnctype: string formMethod: string formNoValidate: boolean formTarget: string name: string type: string value: string willValidate: boolean validity: object ValidityState validationMessage: string labels: object NodeList title: string lang: string translate: boolean dir: string dataset: object DOMStringMap hidden: boolean tabIndex: number accessKey: string draggable: boolean spellcheck: boolean autocapitalize: string contentEditable: string isContentEditable: boolean inputMode: string offsetParent: object offsetTop: number offsetLeft: number offsetWidth: number offsetHeight: number style: object CSSStyleDeclaration namespaceURI: string localName: string tagName: string id: string classList: object DOMTokenList attributes: object NamedNodeMap scrollTop: number scrollLeft: number scrollWidth: number scrollHeight: number clientTop: number clientLeft: number clientWidth: number clientHeight: number attributeStyleMap: object StylePropertyMap previousElementSibling: object nextElementSibling: object children: object HTMLCollection firstElementChild: object lastElementChild: object childElementCount: number nodeType: number nodeName: string baseURI: string isConnected: boolean ownerDocument: object HTMLDocument parentNode: object parentElement: object childNodes: object NodeList firstChild: object lastChild: object previousSibling: object nextSibling: object nodeValue: object textContent: string
disabled: boolean form: object formAction: string formEnctype: string
formMethod: string formNoValidate: boolean formTarget: string name:
string type: string value: string willValidate: boolean validity: object
ValidityState validationMessage: string labels: object NodeList title:
string lang: string translate: boolean dir: string dataset: object
DOMStringMap hidden: boolean tabIndex: number accessKey: string
draggable: boolean spellcheck: boolean autocapitalize: string
contentEditable: string isContentEditable: boolean inputMode: string
offsetParent: object offsetTop: number offsetLeft: number
offsetWidth: number offsetHeight: number style: object CSSStyleDeclaration
namespaceURI: string localName: string tagName: string id: string
classList: object DOMTokenList attributes: object NamedNodeMap
scrollTop: number scrollLeft: number scrollWidth: number
scrollHeight: number clientTop: number clientLeft: number
clientWidth: number clientHeight: number attributeStyleMap: object
StylePropertyMap previousElementSibling: object nextElementSibling: object
children: object HTMLCollection firstElementChild: object lastElementChild:
object childElementCount: number nodeType: number nodeName: string
baseURI: string isConnected: boolean ownerDocument: object HTMLDocument
parentNode: object parentElement: object childNodes: object NodeList
firstChild: object lastChild: object previousSibling: object
nextSibling: object nodeValue: object textContent: string
2. 更多的对象需要创建出来。
从上面的属性列表我们可以看出,我们还需要创建下列对象:
NodeList // an empty list of children of this buttonHTMLCollection // another empty list of children of this buttonStylePropertyMap //NameNodeMap // the attributesDOMTokenList // the CSS classes as a listCSSStyleDeclaration // an object used to deal with CSSDOMStringMap // empty but used for dataset attributesValidityState // ?? no idea
HTMLCollection // another empty list of children of this button
StylePropertyMap // NameNodeMap // the attributes
DOMTokenList // the CSS classes as a list CSSStyleDeclaration
// an object used to deal with CSS DOMStringMap
// empty but used for dataset attributes ValidityState // ?? no idea
到目前为止,这只是创建对象的时间。这里的许多属性需要设置默认值,用空字符串来填充,或者用其它需要创建的对象来填充,这些创建的对象也需要填充所有属性,并且可能还需要创建更深层次的对象。
好了,既然HTMLButtonElememt这个对象已经创建出来,它就被插入到DOM中。
在渲染时,浏览器将遍历DOM,我确信有一定数量的缓存,但它需要确定按钮在哪里。它可能会构建一些独立的内部场景图,与DOM本身分离,后者将执行1000多行代码来渲染特定的特性。
最终它将到达渲染按钮的地方。在这里,它必须再次检查100多个CSS属性。比如Text color,Font size,Font Family,Text
Shadow,Transform,Animation, Border,Multiple Borders,Background color,Background
Image,Background gradient,Is it transparent,Is it on its own stacking
context,等等属性。毫不夸张有100多个属性。
假设没有使用任何特殊的特性的话,最终它会生成一些四顶点(quad
vertices)来渲染字体字形。它可能会将这些字形渲染为纹理(texture)或纹理网格,以用于堆叠上下文(stacking
context)。这是一种优化,因此理想情况下,如果不同的堆叠上下文的内容发生了更改,但此堆叠上下文中没有任何更改,则可以跳过为此上下文重新渲染纹理的过程,而只使用上次创建的纹理。
我确信还有100个与缓存位置相关的其他步骤被我遗漏了,比如将一些事情标记为已经计算过的,这样它们就不会被重新计算,如此等等。
对比一下ImGUI:Button,它只是一个函数,而不是一个对象。它的作用就是:
*
将按钮矩形剪裁到当前剪裁空间,如果剪裁完成,则退出
*
将按钮矩形的顶点插入到预先分配的顶点数组中
*
当第一个字形被按钮区域剪裁时,插入每个字形位置的顶点。
*
按下鼠标按钮,如果其位置在按钮矩形内,则返回true,否则为false。
这就是它全部做的事情。
请注意,这4个步骤也存在于支持HTML/DOM的浏览器中。只不过它们是100个步骤中的4个而已。
总的来说,ImGUI样式可能更快,也更容易使用。无论对于简单的情况或者复杂的情况下,它都易于使用。它的API也更易于使用,这很容易解释。它没有状态,没有对象。没有数据编制,没有事件或回调。即使当UI变得复杂时,它的速度还可以如此之快,所以不需要创建像React的虚拟DOM这样的大型框架。因为速度很快,所以几乎不需要花费精力寻找变通方法来解决像DOM这样的缓慢性问题。对ImGUI风格的UI实现投入更多的精力进行研究,可以促进生产力的巨大提高。
原文:https://games.greggman.com/game/imgui-future/
本文为 CSDN 翻译,转载请注明来源出处。
牛了,这几个案例让你迅速掌握AI技术!
https://edu.csdn.net/topic/ai30?utm_source=csdn_bw
<https://edu.csdn.net/topic/ai30?utm_source=csdn_bw>
【End】
作为码一代,想教码二代却无从下手:
听说少儿编程很火,可它有哪些好处呢?
孩子多大开始学习比较好呢?又该如何学习呢?
最新的编程教育政策又有哪些呢?
下面给大家介绍CSDN新成员:极客宝宝(ID:geek_baby)
戳他了解更多↓↓↓
热 文 推 荐
不得了!这个 AI 让企业家、技术人员、投资人同台“互怼”
<https://blog.csdn.net/csdnnews/article/details/90709244>
史上第一代图形浏览器往事 <https://blog.csdn.net/csdnnews/article/details/90725162>
5G NR 标准:下一代无线通信技术 <https://blog.csdn.net/csdnnews/article/details/90725169>
如何使用 Firefox 阻止指纹识别的侵扰?
<https://blog.csdn.net/csdnnews/article/details/90725170>
☞谷歌临时工达 12 万,外包程序员的出路在哪里?
<https://blog.csdn.net/csdnsevenn/article/details/90709026>
☞直接拿来用!灵跃模组机器人硬核评测(编程篇)
<http://mp.weixin.qq.com/s?__biz=Mzg3MDA4NDkxMQ==&mid=2247483999&idx=1&sn=8ce05699fc0b35ab1ddfd84fee2453e1&chksm=ce927a1ff9e5f309f39da67e93a9a17d52d67881cb258203f4625fccbd12f7d40e9451ec1b4d&scene=21#wechat_redirect>
☞IEEE 回应禁止华为系审稿人;WiFi联盟、蓝牙联盟已恢复华为成员资格;中国计算机学会:暂时中止与IEEE通信学会合作……
<http://mp.weixin.qq.com/s?__biz=MzA3MjY1MTQwNQ==&mid=2649828076&idx=1&sn=5b5b953376dd3e5ea8b1cb25bf4a4322&chksm=871e8d0eb06904184d81603e149c9cf02e07f30c0a296cc1746fe054f7d332cd6e74f2b51654&scene=21#wechat_redirect>
☞敲诈团伙将黑手伸向宅男, 你在家看不可描述的视频, 竟被骗走100万美元!
<https://blog.csdn.net/Blockchain_lemon/article/details/90709332>
☞各方最新回应!如何看待IEEE官方声明“学术禁令”?
<https://blog.csdn.net/dQCFKyQDXYm3F8rB0/article/details/90702537>
☞代码整洁之道-编写 Pythonic 代码
<http://mp.weixin.qq.com/s?__biz=MzU5MjEwMTE2OQ==&mid=2247486466&idx=2&sn=3147cffcbc253101afd9cdda5180e76e&chksm=fe25a65cc9522f4af01dd4b126e57f9db495b3f8b3114721f93b23898d7a1ce8e7633393c5bb&scene=21#wechat_redirect>
☞敲代码时,程序员戴耳机究竟在听什么?
点击阅读原文,输入关键词,即可搜索您想要的 CSDN 文章。
你点的每个“在看”,我都认真当成了喜欢
热门工具 换一换