
Unity3D是著名的3D和2D游戏开发平台,已在全球范围内普及。 同时,其功能不仅限于开发游戏应用程序,还适用于需要创建跨平台应用程序以处理图形的任何其他领域。 在本文中,我们将讨论使用Unity3D开发用于计算开放空间照明的系统的经验。
我们与之合作的公司是国际照明公司
BOOS LIGHTING GROUP 。 为了扩大产品的吸引力并简化与客户的互动,有必要开发一个应用程序,使您可以直观地模拟照明设备的位置,并执行照明计算并在报告中显示必要的技术信息。 假定该应用程序是由潜在的客户或销售代表在iPad或Android平板电脑上启动的,并允许客户立即了解照明安装的可能性。
该工作是根据
BOOS LIGHTING GROUP公司制定的要求规范和项目主题的咨询意见分阶段进行的。
一般而言,该应用程序是一个编辑器,可让您添加和编辑照明元素,道路,装饰元素,执行场景的照明工程计算,以pdf显示报告。 每个元素都有其自己的编辑参数集和影响其显示和计算的子类型。
- 有几种类型的照明桅杆,包括各种类型的灯具,灯的倾斜角度和延伸长度。 对于某些类型的照明设备,可以进行单独调整并指明照明方向。
- 道路可以是线性截面,弧形元素,区域,环。 对于每个元素,可以自定义尺寸,位置,布局类型,图层。
- 装饰元素-汽车,树木,灌木丛,路标
场景的所有元素都可以旋转和移动。 还支持还原或重试的标准操作。 该项目的常规设置允许您设置dorg的纹理(地球表面),并显示其他参数。 场景以2D / 3D模式显示。 并且在计算表面上的照明时,将显示虚拟颜色的表面照明图。
如果可能,应使用本机iOS / Android工具完成整个UI。
该应用程序的主要技术要求是能够根据灯具的技术规格计算舞台照明。 每个灯具还必须具有以3D / 2D模式显示和查看其辐射方向图(光强度曲线)的能力。
平台选择
为了实现该项目,我们选择Unity来方便我们实现所需的功能。 总的来说,我们公司具有与其他3D引擎和平台(OpenSceneGraph,Ogre3D,LibGdx)合作的经验,并且从技术上讲,它们都可以处理所需的任务,但是这次选择落在Unity上,这使得在开发和调试过程中更易于管理场景在工作过程中。
主要困难
因为从技术上讲,显示和编辑场景的功能是非常标准的,所以我们不会涉及整个应用程序开发的复杂性。 自然,在对象的特定编辑,添加和删除对象以及保存命令链以重复和取消动作的可能性方面存在困难。
我们只想关注与使用本机UI,生成pdf报告以及使用光度学和照明计算有关的系统功能。
使用本机UI
在大多数情况下,Unity使用插件系统与系统的本机功能交互,这使您可以在应用程序中嵌入所需的功能。 但是,在我们的情况下,情况有些相反。 我们需要在Unity窗口顶部显示完整的UI。
幸运的是,Unity可以导出可用作本机应用程序基础的项目。 这种情况下的主要困难是如何将附加的UI集成到结果项目中。 同样,同样重要的是,在组装Unity项目时,其格式和文件位置由Unity形成并部分重写,这限制了修改项目的可能性。
在开发iOS应用程序时,我们使用了
本文中提出的机制。 在开发过程中,使用了Unity 5.5,目前,其中显示的内容可能会失去相关性。 组装android项目时,没有其他问题,但Unity每次都会覆盖清单文件和资源文件。
另一个问题是Unity只能在一个窗口中工作。 同时,我们需要确保Unity能够显示整个场景,并且在打开设置窗口时,应该显示灯的光度体的3D模型。 为此,我不得不使用“ hack”并在不同的窗口中使用相同的UIView对象。
要发送消息,我们使用了Unity提供的标准功能。 也就是说,所有消息均采用json格式并以简单的行进行传输。 这不会对性能施加任何限制,因为消息的大小最多可以达到100个字符,并且消息的频率取决于程序的工作速度。 同时,在要求更高的应用程序中,有必要像在Haber
此处和
此处所示 ,制作自己的消息处理程序。
照明计算
应用程序中使用的所有光源均以标准
IES格式提供,该格式描述了不同方向(
规范 )上的光分布。 此格式广泛用于专业CAD系统和3D编辑器中。 它是一个文本文件,指示各个方向上的光强度,另外的元信息指示类型,光源的总强度,轴和对称平面。 给定灯具的对称性,ies文件可能很小。 例如,在轴向对称的情况下,仅在一个平面上指示光跟踪就足够了。
一个简单的IES文件的示例IESNA91[TEST] Simple demo intensity distribution [MANUFAC] Lightscape Technologies, Inc. TILT=NONE 1 -1 1 8 1 1 2 0.0 0.0 0.0 1.0 1.0 0.0 0.0 5.0 10.0 20.0 30.0 45.0 65.0 90.0 0.0 1000.0 1100.0 1300.0 1150.0 930.0 650.0 350.0 0.0
为了显示辐射图,使用了两种类型的显示器:
- 光强度曲线(KSS)是二维图,根据方向显示了一个主平面中的光强度。 为方便起见,可以在极坐标系和笛卡尔坐标系中表示此图。
- 光度体-不同方向的光强度的三维图像
计算模块
为了计算照明,客户在公司的其他产品中使用了自己的C ++模块,因此需要将其集成到Unity项目中。 模块的连接顺序与所使用的平台不同。
- 在iOS平台上,Unitu可以直接调用C函数,因此只需将模块的源代码直接复制到项目中,并添加用于与Unity交互的类。 类可以直接存储在iOS项目中,也可以存储在plugins文件夹中,当项目导出到Xcode时,这些文件夹会自动复制。 调用C ++函数的示例如下:
[DllImport("__Internal")] public static extern void calculateLight([MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1)] Light[] lights, int size, ref CalculationResult result);
- 在Android平台上,必须将C ++模块预编译到一个单独的库中。 这可以通过将C ++源代码添加到项目中并设置gradle以便在so库中构建它们来直接完成。
- 另外,为了调试和测试Unity部件,开发是在Windows机器上进行的,因此也必须在Windows中连接模块的源代码。 这与android项目类似,只是在这种情况下,下载的文件收集在dll库中并连接到项目。
光照图显示
应客户要求,照度计算结果应显示在场景表面。 在道路表面上,必须使用虚拟颜色,并带有刻度以显示颜色和光强,以匹配颜色,而在其余表面上,仅显示其亮度就足够了。
如前所述,整个计算是通过C ++插件执行的,彩色源上的数据被传输到了该插件。 计算的结果是在场景的整个表面上具有给定细节的二维光强度数组。
分析生成的辐照度图的最小值和最大值,以此来构建一维渐变纹理(GradientRamp)。 使用这种纹理,可以直接在片段着色器中将光强度转换为虚拟颜色。 同时,同一着色器用于不同的道路表面和地球表面,并且使用“
多重编译 ”着色器提供了照明模式的切换。
PDF文件生成
根据技术要求,将为用户生成一份报告,其中包含有关一般场景的信息(尺寸,其图像,照明参数)和有关所使用的每种类型的照明设备的信息,指示其位置,特性方向以及KCC图和测光体的显示。
由于报告将同时在iOS和Android上显示,因此有必要直接在Unity模块中生成报告,然后使用标准的本机工具显示报告。
为了生成pdf,选择了符合我们要求的
iTextSharp库。 在其中创建报告并不是特别困难,它包括直接从代码创建文本,表格,图像块。 但是,在开发过程中,我们面临许多细微差别,有时需要付出大量努力才能解决该问题。 其主要问题是在后台线程中启动报告生成。
如果在台式机上进行测试时,pdf生成大约需要几秒钟,那么在iPad mini 3上进行测试时,该时间很容易达到1-3分钟。 自然,报告的创建需要转移到单独的线程中,以避免接口暂停的问题。 在一般情况下,这不是问题,但是在使用Unity时不是这种情况,在这种情况下,明确禁止从主线程外部使用Unity API。 同时,对于报告,我们至少需要渲染CSS和场景图像,这只能从主流中完成。
因此,要生成报告,我们需要按一定顺序运行任务,同时,其中一些任务可以在后台线程中工作,而部分任务必须在主线程中启动。
乍一看,要解决此问题,您可以尝试使用标准机制,并在单独的协程中运行每个操作。 但是,这不能使我们摆脱制动接口的问题。 如您所知,协程在主线程中工作,不适合慢速操作。 同时,在生成报告时,许多操作都需要大量时间,因此协程不能帮助解决我们的问题。
UniRx
另一种解决方案是将代码分成需要在主线程中工作的部分和可以在单独的线程中运行的部分。 在这种情况下,例如,可以使用协程机制构造图像,然后可以将它们嵌入到单独的流中的报表中。 但是,在这种情况下,有必要将中间结果保存在某处,这对使用的内存量或设备上的可用空间施加了额外的限制。
在我们的应用程序中,我们倾向于直接运行并在主线程或后台线程中顺序运行任务。 唯一的问题是如何组织这样的任务启动,以免陷入混乱并正确地同步操作。
通过将Rx作为自由UniRx
资产的实施例,Rx的使用为解决此问题提供了重要帮助,Rx已在
此处和
此处进行了详细介绍。
它的使用大大简化了线程之间的交互,下面的示例显示您可以严格地运行多个方法,但是可以在不同的线程中运行
代码示例 var initializer = Observable.FromCoroutine(initMethod); var heavyMethod1 = Observable.Start(() => doHardWork()); var mainThread1 = Observable.FromCoroutine(renderImage); var heavyMethod2 = Observable.Start(() => doHardWork2()); initializer.SelectMany(heavyMethod1) .SelectMany(mainThread1) .SelectMany(heavyMethod2) .ObserveOnMainThread() .Subscribe((x) => done()) .AddTo(this);
在此示例中,doHardWork()方法将在后台线程中顺序运行。 完成后,将在主线程中启动renderImage(),然后在后台线程中再次执行doHardWork2()。
还值得注意的是,在分析报告生成的性能时,发现最慢的部分是报告中图像的实现。 在互联网上进行的搜索显示,我们不是唯一面临此问题的人,但是没有适合我们的解决方案。 我们不得不将图像质量略微降低到可接受的水平,这使速度提高了20-40%。
因此,在我们创建的应用程序中,可以将Unity图形引擎成功引入本机iOS / Android应用程序。 这与传统方法不同,传统方法是将Unity作为应用程序的主要部分,并通过插件系统解决系统的特定属性。 同时,如果您需要开发要在其中嵌入非平凡3D图形的复杂本机界面,那么我们的方法可能会很有用。