数字化皮影戏交互系统开发日志——沙盘模块①
本文最后更新于 2024年1月26日 晚上
从零开始的一次尝试。
初期策划
既然名为“数字皮影戏科普交互系统”,首要需求便是在程序内复原实物皮影戏,包括视觉观感、操作方式、音效氛围、演绎内容等。
策划期间,需要着重考虑可行性。笔者作为技术人员,将技术可行性作为策划期间的首要考虑因素。
基本可行性
上面是一张实物皮影戏的视频截图。可以观察到,实物皮影戏在视觉上与传统2D平面游戏的区别在于:
- “远虚近实”:元素离幕布越远,色调越偏向黑色,并且越来越淡,同时整体越来越大。
- 位于幕后:所有元素在实物皮影戏中均位于幕布之后,通过投影在另一端呈现。
“远虚近实”效果涉及到Sprite的两方面:大小与颜色。前者(Sprite的大小随距离变化)只需编写脚本动态修改Sprite的localScale即可。后者(同时修改材质的颜色与透明度)同样可以使用脚本实现。但为了提前学习图形学知识,笔者这里选择编写Shader。
上图展示了Shader的编写思路。材质附着在影人上时,通过_WorldSpaceCameraPos可得到L2。保持L1不变(一般是实物皮影戏里的摄像机也不会移动过),就可计算得到L3。把L3作为变量加入Shader的frag函数中,即可让材质片元随L3的变化而变化。因此,技术上可行。
整体框架
最主要的视觉视效复刻是完全可行的。接下来考虑程序的整体框架。
沙盘模块
对于影人,各个部件就是骨骼,部件之间的转轴就是关节。现实中表演者是用几根棍子拖动影人的主要关节来使其移动的,游戏里我们可以用鼠标拖动模拟棍子拖动。对于没有棍子控制的部位,一般使用重力+惯性的方式使其移动。在Unity里,这意味着它们要附着刚体与碰撞体组件。
如果先不考虑影人的自动移动与表演,单纯把影人作为一个2D布娃娃来看的话,只需要用铰链关节把各部位连接起来就可以了。技术可行。此外,2D布娃娃也可以作为系统的一部分,让用户拖着这个布娃娃在空白的场景里自由的玩耍。这就是沙盘模块。
戏剧播放模块
既然是复原实物皮影戏,那程序肯定得能播放经典的皮影戏剧。直接放视频未免显得太敷衍,我们要搭建一套完善的皮影戏剧播放模块。在实现了沙盘模块的基础上,我们有两个选择:
- 不沿用2D布娃娃系统,改用2D骨骼动画。这种方式比较困难,因为皮影剧目往往持续十分钟以上,如果用K帧的方式做动画的话工作量太大。
- 沿用2D布娃娃系统,想办法借此制作动画。
一番权衡利弊,还是在沙盘模式的基础上制作剧目动画比较好。而且如果给沙盘模式加入录制功能的话,玩家也可以自己录剧目自己看,也不失为一种乐趣。
就像视频是由一帧帧图片构成的一样,数字皮影剧目里,“一帧”是由场景内众多元素的位置信息构成的。只要每隔一段极短的时间,把时间信息与当前帧所有元素的位置信息写入文件,就可以完成“剧目”的记录。播放时,读取文件即可播放剧目。
图鉴模块
作为科普应用,我们的系统自然也是得有图鉴模块的。图鉴模块就比较简单,一个滑动窗口+若干UI元素就完事了。当然,要做的花哨的话,也可以模仿老滚5的加载界面,点一个UI单元,右边就会呈现它的模型。
编码
沙盘模块做完以后,做其他模块都会比较方便,所以笔者首先进行沙盘模块的编码。
踩坑
拖拽与铰链关节的配合
1 |
|
OnMouseDrag()事件函数本质上是改变Transform的position,并不是对刚体产生影响,而铰链关节的相互作用是基于刚体的。因此,使用OnMouseDrag()编写的拖拽功能,在生效时,整个影人都是静止的,完全不存在惯性。
可能是笔者才疏学浅,到目前为止没听说过基于刚体的拖拽实现。因此,我们要在保留OnMouseDrag拖拽的同时,对刚体进行处理。
如果按照现实物理的思路,应当为对象手动添加“与拖拽相关联的惯性”。在OnMouseDrag()中记录鼠标移动增量,并以此作为速度。通过Update+Lerp实现对象Rigidbody.velocity不断向鼠标移动速度逼近,以此模仿惯性。然而,这种方式实现起来还是比较困难,同时也比较耗性能。
惯性在含有铰链关节的对象上最显著的表现是:对象物体向一端加速移动时,铰链连接着的另一物体会呈现“向反方向移动”的表现。那么,是不是只要在拖动父物体时,给子物体施加反方向速度,就能模拟惯性呢?
1 |
|
效果非常好。
多相机混合
第一次用著名Unity插件Top-Down Engine时,单独UI Camera渲染UI的实现方式深深震撼了Unity初学者的心。不管怎么说,这个项目笔者都要用上这种方式。
新建一个Camera,把Audio Listener去掉(场景内通常只存在一个Audio Listener,一般附着在Main Camera上),把它的投影方式设置为正交(因为UI不需要透视)。
注意,Clear Flags要设置为Depth Only。这里的Depth指的是相机的Depth,Depth越高的相机渲染次序越靠后。即便UI位于一大堆主相机看着的Opaque物体之后,只要UI Camera的Depth大于Main Camera,UI就能好好显示。网上有个设置UICamera的教材让把Clear Flag设置为Dont Clear,属实误人子弟。
Culling Mask设置为UI,这样UI Camera就不会渲染其他不小心跑进来的东西。
代码控制Scale
对于2D Sprite,调整Scale时,变化的基点是Pivot。例如,Pivot位于中心的正方形,Scale等比例增大时,其四条边均匀远离中心。Pivot位于下边中点的正方形,Scale等比例增大时,下边的世界坐标(这么说其实不严谨,理解就行)不会发生变化。
通过改变Sprite的Pivot(在Sprite Editor中进行),可以避免例如位于屏幕边缘的Sprite调整Scale后超出屏幕范围的情况。