前段时间,网上出现了一段使用HTC vive观看LoL的视频,相关链接在这里。这是由一位名叫Fire-Proof的大神开发完成的,并且大神公布了源代码。作为一名程序员,对牛掰的代码自然充满了好奇,用VR观看LoL,这到底是如何实现的呢?
查看代码发现,程序完全使用Python开发(再次感受到Python的强大),包含的代码并不多,一共也就10几个文件。
首先查看main.py。在最下方定义了start_app
方法
def start_app():
app = QApplication(sys.argv)
tray_icon = MainDialog()
tray_icon.show()
app.exec_()
程序创建了一个名为MainDialog的对话框,让我们看一下MainDialog中有些什么(代码有删减)
class MainDialog(QDialog, Ui_MainDialog):
def __init__(self, parent=None):
super(MainDialog, self).__init__(parent)
self.spectate = None
self.spectate_started = False
self.setupUi(self)
self.lol_watcher = ProcessWatcher(b"League of Legends")
self.lol_watcher.running.connect(self.lol_watcher_update, Qt.QueuedConnection)
self.lol_watcher.start()
self.vorpx_watcher = ProcessWatcher(b"vorpControl")
self.vorpx_watcher.running.connect(self.vorpx_watcher_update,Qt.QueuedConnection)
self.vorpx_watcher.start()
self.pushButtonStart.clicked.connect(self.toggle_spectate)
def toggle_spectate(self):
if self.spectate_started:
self.pushButtonStart.setText("Start")
self.stop_spectate()
self.spectate_started = False
else:
self.pushButtonStart.setText("Stop")
self.start_spectate()
self.spectate_started = True
def start_spectate(self):
if self.spectate is None:
self.spectate = SpectateThread()
self.spectate.error.connect(self.spectate_error, Qt.QueuedConnection)
self.spectate.start()
对话框创建了一个start按钮,并将该按钮的点击事件绑定到了toggle_spectate
方法上,该方法会调用start_spectate
方法,并最终通过SpectateThread
创建了一个线程。继续查看SpectateThread
(代码有删减)
class SpectateThread(QThread):
def __init__(self):
super(SpectateThread, self).__init__()
self.spectate = None
def run(self, *args, **kwargs):
self.spectate = VRSpectate()
self.spectate.run()
看来玄机在VRSpectate
方法中。在查看VRSpectate
之前,注意到MainDailog
中开启了两个线程来检测"League of Legends"进程和"vorpControl"进程。前一个很好理解,观看LoL自然需要"League of Legends"进程,那这个"vorpControl"进程又是什么鬼呢?这是vorpX软件的运行进程,用来将非VR游戏的画面转换成VR模式并同步到头显设备中。看到这里是不是有种被欺骗的感觉,通过VR观看LoL是通过软件来实现的?答案确实如此,Python程序并不负责转换LoL的画面到VR设备上,它只是不停的检测VR头显和控制器的状态,并以此调整LoL的画面,这样就可以通过转动头部或触发控制器来以不同的视角观看LoL画面(比如站立时鸟瞰整个战场,蹲下来观看局部战场)。
在VRSepecate
的run
方法中,会不停的调用_next_frame
方法来更新画面
def _next_frame(self):
self.scale = self.z_offset
controller_frame = self.vr.controller_frame()
# Camera movement
if len(controller_frame) == 2:
if all(controller_frame.button_pressed("trigger")):
self.yaw_offset += self.prev_controller_frame.relative_rotation - controller_frame.relative_rotation
self.z_offset += (self.prev_controller_frame.relative_distance - controller_frame.relative_distance) * self.z_offset * 2
self._move_offset(self.prev_controller_frame.position(), controller_frame.position())
elif any(controller_frame.button_pressed("trigger")):
active_controller = [i for i, x in enumerate(controller_frame.button_pressed("trigger")) if x][0]
self._move_offset(self.prev_controller_frame.position(active_controller), controller_frame.position(active_controller))
if len(controller_frame) == 1:
if any(controller_frame.button_pressed("trigger")):
self._move_offset(self.prev_controller_frame.position(), controller_frame.position(), update_z=True)
self.prev_controller_frame = controller_frame
pos = self.vr.hmd.position()
self.lol.yaw = pos.yaw + self.yaw_offset
self.lol.pitch = pos.pitch*-1
self.lol.x = (pos.x*self.scale) + self.x_offset
self.lol.y = (pos.y*self.scale) + self.y_offset
self.lol.z = (pos.z*self.scale) + self.z_offset
该方法会首先来检测VR控制器是否触发,并获取VR头显的位置信息,根据这些数据来更新LoL的画面信息。其中访问VR设备是通过调用openvr库来实现的,并在OpenVR.py中做了进一步的封装。那么更新LoL画面如何实现呢,答案是修改LoL进程的内存。
在LeagueOfLegends.py中,封装了修改LoL进程内存的方法,例如执行self.lol.x = 520,实际上执行的是下面的代码
@x.setter
def x(self, val):
self._x.write(val, type="float")
而更底层的实现是通过memorpy目录中的相关代码来实现的。那么如何知道应该修改哪些内存位置呢(毕竟直接修改内存一个不慎可能进程就挂了)。涉及到画面信息的内存位置都是通过一个cam_base_address
进行偏移得到的,作者给出了cam_base_address
的搜索方法:
HOW TO: Find base addr
- Move screen to lowest y coord
- Scan for float = 520
- Move screen to highest y coord
- Scan for float = 14765
- Select the address that will updated the camera pos when changed
- Right click this address and click "find out what accesses this address"
- Select an element and press more information
- Copy the value which is listed at the end of the string "The value of the pointer needed to find this ..."
- Search for this this value as an 8-bit hex value
- Now search for the first address found
- You should now have found a "green" address, select this and double click the address
- You should now be able to see a string similar to "League of Legends.exe"+13D4280 <-- base addr
看起来作者是通过某些内存查看软件(例如IDA)来进行分析得到的内存地址(在此膜拜大神)。
了解了这些,基本就明白程序的技术原理了,剩下的部分感兴趣的同学可以自行查看源码,当然有条件可以上设备体验一番。