前言
小时候最喜欢看西游记,总是幻想着自己能像孙悟空那样,脚踏筋斗云穿梭云海间,生活在仙境中。长大后做了图形程序,一直想做一个真正的云海出来,但由于移动端的计算瓶颈,一直没能做出一个兼顾性能和效果的体积云(体积云是基于物理的云渲染系统,在游戏中模拟出具有半透明、无规则的表现效果的云)。
本人是一个游戏开发爱好者,经常会fellow一些前沿的技术,并且将一些感兴趣的技术点开发成一个可以方便使用的插件。最近看到华为HMS Core 中的CGKit提供了一个体积云插件,所以就花了两天时间按照官方文档集成到Unity中,下图是一个简单场景的效果(上面的云为天空盒,下面的云为集成后的体积云),可以看到体积云整体偏真实,“金边”效果也比较明显,支持动态光照,可以在云中任意穿梭。最重要的是我在一个低端机(荣耀8青春版)上测试了一下性能,分辨率为720P的情况下,竟然可以跑到50帧!该插件还有一个有意思的功能是,支持开发者定制云的形状,这样我就可以拥有一朵任意形状的云了。
接下来就和大家分享一下我是如何将HMS CGKit体积云插件集成到Unity中的,希望对大家的开发有所帮助。
2、开发准备
1、Visual Studio,推荐使用2017及以上版本;
2、Android Studio,推荐使用4.0及以上版本;
3、Unity,推荐使用2018.4.12及以上版本;
4、EMUI 8.0及以上华为手机或Android 8.0及以上非华为手机;
5、下载SDK
到华为开发者联盟下载SDK,下载链接及说明文档链接如下:
将SDK下载下来,目录结构如下:
process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDcwODI0MA==,size_16,color_FFFFFF,t_70)
通过官方的说明文档可知: RenderingVolumeCloud.dll是基于OpenGL的PC端调试插件,libRenderingVolumeCloud.so是基于OpenGLES30的Android端插件,assets目录下的两个bin文件是渲染体积云时需要用到的资源文件,include目录下的头文件是该插件的接口定义。
3 开发详细记录
由于该体积云插件对外暴露的是C++接口,所以要想集成到Unity中,需要将其封装为Unity的原生插件(Native plugin),然后集成到Unity获得体积云的渲染结果。下面分别给大家介绍一下我的集成过程。
3.1 原生插件的封装
Unity原生插件是指用C、C++、Objective-C 等编写的原生代码库,插件允许游戏代码(用Javascript或C#编写)调用这些库中的函数。Unity原生插件的封装可以参照如下官方文档以及官方示例教程:
文档:https://docs.unity3d.com/Manual/NativePluginInterface.html
代码:https://github.com/Unity-Technologies/NativeRenderingPlugin
根据Unity官方示例,需要分别编译so和dll作为动态链接库供Unity调用。可以分别用Visual Studio和Android Stuido直接对官方发布的开源代码进行修改,集成体积云渲染功能,并编译出对应的库文件,下面简要介绍修改方式。
RenderingPlugin.def文件中的函数就是原生插件暴露给Unity的接口,其具体实现在RenderingPlugin.cpp中。我们保留RenderingPlugin.cpp中的UnityPluginLoad、UnityPluginUnload、OnGraphicsDeviceEvent、OnRenderEvent和GetRenderEventFunc这几个Unity原生插件必须的函数以及相关的静态全局变量,并添加了3个接口(ReleaseSource,BakeMultiMesh,SetRenderParasFromUnity),如下图所示。
接下来通过对官方源代码修改,利用这几个接口实现对CGKit体积云插件的调用。修改如下:
(1) 修改OnGraphicsDeviceEvent函数。当eventType 为kUnityGfxDeviceEventInitialize时,调用CGKit体积云插件CreateRenderAPI函数创建RenderAPI类型的变量,并调用RenderAPI.CreateResources()函数;当eventType 为kUnityGfxDeviceEventInitialize时delete RenderAPI类变量。
(2) 修改OnRenderEvent函数。将SetTextureFromUnity函数中设置的全局静态变量作为参数,在该函数中直接调用RenderAPI. RenderCloudFrameTexture()函数。
(3) 定义SetTextureFromUnity函数。将RenderAPI. RenderCloudFrameTexture()函数所需的4个输入值传给定义好的静态全局变量,方便以后直接调用该函数进行体积云渲染。
(4) 定义SetRenderParasFromUnity函数。在该函数中调用RenderAPI. SetRenderCloudParas()函数。
(5) 定义ReleaseSource函数。在该函数中调用RenderAPI. ReleaseData()函数。
为了实现云形态的定制,PC端插件需要集成体积云的烘焙功能,所以dll要比so多一个烘焙接口。在编译dll时需要额外定义BakeMultiMesh函数,首先调用CreateBakeShapeAPI创建BakeShapeAPI类变量,然后调用BakeShapeAPI.BakeMultiMesh()函数进行烘焙。
3.2 集成到Unity
原生插件封装成功后,就可以得到适配Unity和该体积云插件的libUnityPluginAdaptive.so和UnityPluginAdaptive.dll。接下来就是新建一个Unity 3D工程,来实现体积云功能的调用,这里我实现了ARM64版本,以此为例给大家介绍一下。
将ARM64版本的libUnityPluginAdaptive.so、libRenderingVolumeCloud.so、UnityPluginAdaptive.dll和RenderingVolumeCloud.dll放到Assets/Plugin/x86_64文件夹下,没有该文件夹的话自己新建一个就好。需要对两个so和dll分别进行如下配置:
由于该体积云插件的PC端调试插件是基于OpenGL的,所以需要做如下设置:
由于该体积云插件的Android端插件是基于OpenGLES30的,所以需要做如下设置:
该插件还提供了两个bin文件,将这两个文件放到工程任意文件夹下,只要在传入参数中定义好对应的路径即可,其中noise.bin是体积云的细节噪声,shape.bin是体积云的三维形状纹理。也可以调用插件的BakeMultiMesh接口自定义体积云的形态,这一部分会在后面详细展开,先暂时使用插件提供的三维形状纹理。
3.3 实时渲染体积云
调用体积云插件前,需要添加打点功能依赖jar包,可以通过修改代码工程的应用级build.gradle文件来实现,任意jar包版本号均可。
将下载后的jar包,拷贝到Unity工程中,Assets/Plugin/Android/bin文件夹下,没有该文件夹的话自己新建一个就好。
接下来就可以编写C#脚本调用相关接口,这里我显式调用的适配层接口如下:
(1) SetTextureFromUnity函数的作用是设置cloudTexture的指针、depthTexture的指针,以及cloudTexture的尺寸。其中cloudTexture是用于体积云绘制的纹理数据, depthTexture是有当前帧depth数据的纹理。该函数只需执行一次即可。
(2) SetRenderParasFromUnity函数的作用是调用CGKit体积云插件参数设置的接口,设置体积云的参数。由于这些参数每一帧都要更新,所以该函数每帧都要执行一次。
(3) GetRenderEventFunc函数的作用是调用CGKit体积云插件绘制接口,将体积云绘制到cloudTexture上。该函数的调用方式为GL.IssuePluginEvent(GetRenderEventFunc(), 1),也可以以CommandBuffer的形式调用 commandBuffer.IssuePluginEvent (GetRenderEventFunc(), 1)。每帧都要执行一次。
(4) ReleaseSource函数的作用是调用CGKit体积云插件释放资源接口。只需要最后调用一次即可。
接口定义好以后,调用流程如下:
上图中灰色的部分是需要根据我们自己来实现的,这里给大家介绍一下我的一个简单实现。在调用渲染接口之前,需要创建两个RenderTexture,其中一个用来保存体积云插件渲染结果,另外一个用来保存depth。然后调用SetTextureFromUnity接口,将两个RenderTexture的NativeTexturePtr以及尺寸作为输入参数,这样后面就可以得到体积云渲染的结果。
在Update阶段,需要更新体积云插件的结构体参数,该参数需要参考CGKit体积云插件包include目录下VolumeRenderParas.h文件,在C#脚本中需要定义同样的结构体。参数具体含义可以参考CGKit体积云插件说明文档,需要注意的是结构体要一字节对齐,结构体中表示矩阵的4个数组采用行优先排列。下图是一个简单的示例:
更新结构体变量后,调用SetRenderParasFromUnity接口以引用的方式传递体积云渲染的参数,之后的渲染就会基于这组参数进行。
调用渲染接口时,我们以OnRenderImage后处理的形式将体积云绘制到屏幕上,当然也可以使用CommandBuffer的形式在其他阶段进行配置。在OnRenderImage阶段,首先要将depth绘制到之前创建的depthTexture RT上,然后调用GL.IssuePluginEvent(GetRenderEventFunc(),将体积云绘制到之前创建的cloudTexture RT上,最后将cloudTexture和OnRenderImage的输入src利用透明度融合到dst上,就可以在屏幕上显示体积云。
3.4 打包APK到手机端验证
在PC端调试好效果之后,就可以直接打包到APK进行Android端验证。Android端和PC端唯一的区别就是结构体参数中的两个字符串数组,分别是shape.bin和noise.bin文件的路径,这两个路径需要在不同平台区分开来,可以将两个bin文件放到Application.persistentDataPath文件夹下。
3.5 烘焙三维形状纹理
该体积云插件同时也提供了烘焙接口,可以自定义三维形状纹理。同样将其集成到适配层,前边已经有过介绍,接口函数为:
函数的输入为BakeData结构体变量以及保存bin文件的路径,同时会生成类似于shape.bin的文件,其中savePath需包含完整路径以及后缀(.bin)。由于数组长度不定,所以该结构体的成员变量为数组指针类型和int型,参数具体含义可以参考CGKit体积云插件说明文档。根据说明文档我们可以知道,烘焙接口的作用是将一个Boundingbox内的多个mesh烘焙为三维形状纹理。Boundingbox的大小由结构体内的两个参数决定,分别为minBox和maxBox。
如下图所示,调用接口前,先将多个三维模型组合到一起。为了可视化到底哪些区域是可烘焙区域,我们可以在空间中根据minBox和maxBox这两个参数绘制一个线框,在线框外的区域是不予烘焙的。调整好各个模型的位置之后,就可以调用该接口进行烘焙。
在烘焙时还需要注意的一点是,烘焙好的三维形状纹理在渲染体积云时会被连续采样,所以布置三维模型时,需要在水平的四个与boundingBox相交的位置上放置相同的三维模型,使其被循环采样时保持连续性。
烘焙结束之后就可以使用自定义的三维形状纹理进行体积云渲染,用法与官方提供的shape.bin用法一样。
Demo获取
emo以unitypackage的形式打包并上传至百度云盘,地址请戳:
https://pan.baidu.com/s/1JEt_... 提取码: yx8a
从Assets->Import Package->Custom Package中导入该unitypackage,其中Plugin文件夹中有适配层和CGKit体积云插件的dll和so。volumeCloud文件夹中有相关的脚本、shader以及搭建的场景。StreamingAssets文件夹中存放了相关的资源文件(shape.bin和noise.bin)。volumeCloud/Readme.txt中注明了Demo运行的一些注意事项,在运行前请按说明进行配置。
原文链接:https://developer.huawei.com/consumer/cn/forum/topic/0202500645526020389?fid=18
原作者:转来转去