分享一个基于pygame的3D预览脚本,用于之前需要及时评估3D效果的数字人项目。基本的代码、原理都很简单,只是记录一下在使用pygame时用到的一些操作,以及关于三维物体到二维图形的映射方面的一些理解。
原理讲解
要想实现3D画面预览,就需要将三维空间中的物体进行投影变换,使其最终呈现在二维的屏幕上(也就是对于每个点 P(x, y, z)-->投影坐标 P'(x', y'))。一般我们使用透视投影来将三维空间中的点映射到屏幕上的二维坐标系中,从而呈现出符合人眼观察效果的立体感和深度感。当然在一些三维软件中还会提供平行视野,比如blender中的透视/正交(Perspective/Orthographic),unity中的persp/ISO等等。平行视野虽然不符合直觉,但是对于判断比例和距离来说更有优势。
在透视投影中,视点(摄像机位置)和投影平面之间的距离被称为视距(viewing distance),通常用 d 表示。对于点 P(x, y, z),它的投影坐标 P' (x', y') 可以通过以下公式计算得到:
x' = d * x / z y' = - d * y / z
其中,z 不应该等于 0,否则投影无效。
这里的实现步骤大致如下:
- 根据视野角度和屏幕宽度来确定视距。
- 定义需要投影的点的空间坐标的集合。
- 对于每个点,首先将其从世界坐标系转换到相机坐标系。这可以通过将相机位置减去点的位置来实现。
- 接着根据透视投影公式将点 P' 投影到屏幕上,得到最终的二维坐标 (x'', y''):
x'' = x' + center_x y'' = y' + center_y
其中,center_x 和 center_y 是屏幕中心的坐标。(注意一下,pygame中的坐标原点(0, 0)在左上角,x轴水平方向向右,y轴垂直方向向 下,一般在DIP中默认的图像坐标都是这样的。)绘制点的投影就只需要使用 draw.circle() 函数就可以绘制圆形。
在这个过程中,还需要对摄像机位置和视角进行控制。通过控制摄像机位置,可以改变整个场景的观察角度和位置。通过控制视角,可以调整镜头的焦距和广角,进而影响到投影效果。由于我们使用的是透视投影,因此视角越大,投影距离就越近,物体看起来就越大。
代码介绍
网上这块代码也挺多的,我这里就贴一下我参考的代码《pygame教程实例(八)不用3D引擎也可以写3D画面》。作者是以一个静态的正方体(其边由若干离散的球体拼接而成)以及几个位置随机生成的球体为三维场景进行预览的,代码并不复杂,功能和对应的注释基本都有,还是挺直观的。
1 | import pygame,os |
这段代码在三维预览上的主要逻辑就是确定视距-->输入相机移动和视角变化-->更新点-->依次绘制。
首先通过calculate_viewing_distance函数,根据视野角度和屏幕宽度计算视距,用于将3D点转换为2D屏幕坐标。随后,利用pygame.key.get_pressed监听键盘输入,控制相机的空间移动和视距变化,利用pygame.mouse.get_rel()监听鼠标在x轴和y轴上的移动,控制视角移动,并分别在move_points函数和rotate_points函数中对储存的点的坐标位置进行更新。最后遍历所有的点,根据摄像机位置和视距将3D点转换为屏幕坐标。根据点的z值决定绘制的球体的大小,并在屏幕上绘制球体。
为了方便观察,作者将鼠标设置为不可见,并且将鼠标事件设置为捕获模式,以便完全控制鼠标。同时还在视图左上角绘制出了摄像机的位置、视野角度和视距数值的信息。
时间序列的三维预览
不过上面的代码离我期望的效果还有点距离,最大的问题就在于原代码中使用的都是静态物体,而我需要对移动的物体进行展示。由于我不是动画那种关键帧的方式进行连续的移动,而是将30帧记录的若干空间数据在重新在每一帧中“展现”,所以实际上这是对离散的物体进行连续的投影变换。
分析到这其实就已经知道需要修改的地方在哪里了,也就是鼠标旋转和相机移动需要从原先每帧分别计算一次改为累计数值再计算。 在代码中记录旋转分量的drx和dry后加上一个drx_a+=drx dry_a+=dry,并以drx_a和dry_a替换原先的值;相机移动的三轴分量也进行相同的操作,用dxa+=dx, dya+=dy, dza+=dz进行替换就可以了。
这里还要注意一下,原代码中的rotate_points和move_points函数是会根据z轴大小重新排序的。这在原本的物体中是没什么问题的,但是在我的项目中,点之间是有一定的连接顺序,那么排序后就会影响原先的排列位置,虽然也可以在坐标中加入次序,再在检索顺序后绘制图像,但是感觉有点太麻烦了我就直接把函数的排序去了(如果需要更好的视觉效果那还是需要改一下的,不然点之间可能会有不正确的遮挡,我这里点的颜色都是一致的就不管了)。
不过为了方便看出数字人骨骼在空间位置的变换,我还写了一个基准坐标轴,用的还是原先的数值和函数,并且采用了上面说的方法,在每点坐标里加入了次序。不过似乎由于坐标轴和人体的中心点不同,导致其移动的同时进行旋转操作会出现严重错位的现象,明明都是按照世界坐标来的,没太弄清楚是怎么回事就先注释了。
另外,我加入了一个循环播放和鼠标左键点击暂停的功能,其实就是读完文件中的数据后重新读取,暂停就是不读取下一行数据、重复读取当前行数据就可以了。其他地方就是数字人骨骼及连线绘制与主题没啥关系就不讲了。
1 | import copy |
最后结果大概是这样的:
还算可以吧,比起需要导入blender或者ue再看骨架来说简单太多,能够及时评判数据的有效性就足够了(现在数据还有一大堆毛病,数据融合角度变换等等等等就不谈了,之后得找时间重新多录几次更完整复杂的数据才行)。