Skip to content

Instantly share code, notes, and snippets.

@kwahoo2
Created August 15, 2025 19:25
Show Gist options
  • Select an option

  • Save kwahoo2/5001f494d38ce3272dd926997b4bd05b to your computer and use it in GitHub Desktop.

Select an option

Save kwahoo2/5001f494d38ce3272dd926997b4bd05b to your computer and use it in GitHub Desktop.
# pySDL3 is required, install with:
# pip install PySDL3
# Free Flight FreeCAD demo. Navigate scene with mouse and keyboard
# Bindings:
# mouse: yaw and pitch
# Q/E - roll, W/S - forward/backward, A/D - sidestep left/right, Ctrl/Space - down/up
# note: proper threading/concurrency not completed, adjust sleep in self._running loop if segfaults
# note: background color and lights copying from main view not implemented yet
from threading import Thread
from OpenGL import GL
import sdl3
import ctypes
from pivy.coin import SoRenderManager
from pivy.coin import SoSeparator
from pivy.coin import SbViewportRegion, SbVec2s
from pivy.coin import SbVec3f, SbRotation
from pivy.coin import SoPerspectiveCamera
from pivy.coin import SoDirectionalLight
from pivy.coin import SoGetBoundingBoxAction
import time, math
from dataclasses import dataclass
from PySide.QtGui import QGuiApplication
@dataclass
class MovementModifier:
walk: float = 0.0 # forward - backward
sidestep: float = 0.0 # left - right
altitude: float = 0.0 # up - down
pitch: float = 0.0 # pitch
yaw: float = 0.0 # yaw
roll: float = 0.0 # roll
class SDLTest(object):
"FreeCAD SDL testing script"
def __init__(self):
self._running = True
# adjust to your preferences:
self.ang_vel = 0.002 # angular velocity
self.lin_vel = 0.001 # linear velocity
self.mouse_sens = 0.05 # mouse sensivity
self.w, self.h = 1280, 720
# end of adjustment
self.r_rate_mod = 16 # frame duration-dependent velocity correction
self.size_mod = 100 # scene size-dependent linear velocity correction
self.near_plane = 100 # setting too small near_plane value may cause DEPTH_TEST issues,
self.far_plane = 100000 # will be readjusted later based on the scene bounding box
# Cursor locking position
self.fixed_mouse_x, self.fixed_mouse_y = self.w // 2, self.h // 2
# Incrementing mouse position
self.mouse_x, self.mouse_y = 0, 0
self.mouse_x_rel = ctypes.c_float(0)
self.mouse_y_rel = ctypes.c_float(0)
self.frame_duration = 0
self.mov_mod = MovementModifier()
print('Initializing...')
def draw(self):
self.render_manager.render()
sdl3.SDL_GL_SwapWindow(self.window)
def process_events(self):
event = sdl3.SDL_Event()
while sdl3.SDL_PollEvent(ctypes.byref(event)):
match event.type:
case sdl3.SDL_EVENT_QUIT:
self.terminate()
case sdl3.SDL_EVENT_WINDOW_CLOSE_REQUESTED:
self.terminate()
case sdl3.SDL_EVENT_WINDOW_RESIZED:
w, h = ctypes.c_int(), ctypes.c_int()
sdl3.SDL_GetWindowSize(self.window, w, h)
self.w = w.value
self.h = h.value
vp_reg = SbViewportRegion(self.w, self.h)
self.render_manager.setViewportRegion(vp_reg)
case sdl3.SDL_EVENT_KEY_DOWN:
if event.key.key == sdl3.SDLK_ESCAPE:
if self.mouse_is_grabbed:
self.mouse_is_grabbed = False
sdl3.SDL_SetWindowTitle(self.window, b"Click into window to grab the mouse")
sdl3.SDL_SetWindowMouseGrab(self.window, self.mouse_is_grabbed)
sdl3.SDL_ShowCursor()
else:
self.terminate() # terminate if esc hit second time
else:
self.check_key_down(event.key.key)
case sdl3.SDL_EVENT_KEY_UP:
self.check_key_up(event.key.key)
case sdl3.SDL_EVENT_MOUSE_BUTTON_DOWN:
if not self.mouse_is_grabbed:
self.mouse_is_grabbed = True
result = sdl3.SDL_SetWindowMouseGrab(self.window, self.mouse_is_grabbed)
print ('Mouse grabbed', result)
sdl3.SDL_SetWindowTitle(self.window, b"Press Esc to release the mouse")
result = sdl3.SDL_HideCursor()
sdl3.SDL_WarpMouseInWindow(self.window, self.fixed_mouse_x, self.fixed_mouse_y)
x_rel = ctypes.c_float(0)
y_rel = ctypes.c_float(0)
# read after initial warp, to avoid sudden value change after grabbing mouse
sdl3.SDL_GetRelativeMouseState(ctypes.byref(x_rel), ctypes.byref(y_rel))
if self.mouse_is_grabbed:
sdl3.SDL_GetRelativeMouseState(ctypes.byref(self.mouse_x_rel), ctypes.byref(self.mouse_y_rel))
self.mouse_x += self.mouse_x_rel.value
self.mouse_y += self.mouse_y_rel.value
# print(f"Mouse position: ({self.mouse_x}, {self.mouse_y})")
sdl3.SDL_WarpMouseInWindow(self.window, self.fixed_mouse_x, self.fixed_mouse_y)
self.update_position()
def check_key_down(self, key):
print(f"pressed {key}")
match key:
case sdl3.SDLK_W:
self.mov_mod.walk = -1
case sdl3.SDLK_S:
self.mov_mod.walk = 1
case sdl3.SDLK_A:
self.mov_mod.sidestep = -1
case sdl3.SDLK_D:
self.mov_mod.sidestep = 1
case sdl3.SDLK_Q:
self.mov_mod.roll = -1
case sdl3.SDLK_E:
self.mov_mod.roll = 1
case sdl3.SDLK_LCTRL:
self.mov_mod.altitude = -1
case sdl3.SDLK_SPACE:
self.mov_mod.altitude = 1
def check_key_up(self, key):
print(f"released {key}")
match key:
case sdl3.SDLK_W | sdl3.SDLK_S:
self.mov_mod.walk = 0
case sdl3.SDLK_A | sdl3.SDLK_D:
self.mov_mod.sidestep = 0
case sdl3.SDLK_Q | sdl3.SDLK_E:
self.mov_mod.roll = 0
case sdl3.SDLK_LCTRL | sdl3.SDLK_SPACE:
self.mov_mod.altitude = 0
def update_position(self):
yaw = -self.mouse_x_rel.value * self.mouse_sens
pitch = -self.mouse_y_rel.value * self.mouse_sens
self.mouse_x_rel.value = 0 # zeroing values to avoid perpetual spinning
self.mouse_y_rel.value = 0
rot_y = SbRotation(SbVec3f(0.0, 0.0, 1.0), yaw * self.ang_vel * self.r_rate_mod)
rot_p = SbRotation(SbVec3f(1.0, 0.0, 0.0), pitch * self.ang_vel * self.r_rate_mod)
rot_r = SbRotation(SbVec3f(0.0, 1.0, 0.0), self.mov_mod.roll * self.ang_vel * self.r_rate_mod)
input_rot = rot_y * rot_p * rot_r
qm = input_rot.getValue()
mod_rot = FreeCAD.Rotation(qm[0], qm[1], qm[2], qm[3])
m_ypr = mod_rot.getYawPitchRoll()
qc = self.camera.orientation.getValue().getValue()
cam_rot = FreeCAD.Rotation(qc[0], qc[1], qc[2], qc[3]) # SbRotation to Base::Rotation
c_ypr = cam_rot.getYawPitchRoll()
c_r_f = FreeCAD.Rotation()
c_r_f.setYawPitchRoll(c_ypr[0] + m_ypr[0], c_ypr[1] + m_ypr[1], c_ypr[2] + m_ypr[2])
c_sbr_f = SbRotation(c_r_f.Q[0], c_r_f.Q[1], c_r_f.Q[2], c_r_f.Q[3])
self.camera.orientation.setValue(c_sbr_f)
mod_pos = SbVec3f(self.mov_mod.sidestep, self.mov_mod.altitude, self.mov_mod.walk) * self.lin_vel * self.r_rate_mod * self.size_mod
mod_pos_f = c_sbr_f.multVec(mod_pos)
self.camera.position.setValue(self.camera.position.getValue() + mod_pos_f)
def run(self):
sdl3.SDL_Init(sdl3.SDL_INIT_VIDEO)
sdl3.SDL_GL_SetAttribute(sdl3.SDL_GL_CONTEXT_PROFILE_MASK, sdl3.SDL_GL_CONTEXT_PROFILE_COMPATIBILITY)
# this is for FreeCAD X11 build running at top of Wayland
if QGuiApplication.platformName() == 'xcb':
sdl3.SDL_SetHint(sdl3.SDL_HINT_VIDEO_DRIVER, b'x11')
self.window = sdl3.SDL_CreateWindow(
b"Click into window to grab the mouse",
self.w, self.h,
sdl3.SDL_WINDOW_OPENGL | sdl3.SDL_WINDOW_RESIZABLE
)
self.context = sdl3.SDL_GL_CreateContext(self.window)
sdl3.SDL_GL_MakeCurrent(self.window, self.context)
sdl3.SDL_GL_SetSwapInterval(1)
GL.glBlendFunc(GL.GL_SRC_ALPHA, GL.GL_ONE_MINUS_SRC_ALPHA)
GL.glEnable(GL.GL_BLEND)
GL.glEnable(GL.GL_LIGHTING)
GL.glEnable(GL.GL_DEPTH_TEST)
self.mouse_is_grabbed = False
viewer = FreeCADGui.ActiveDocument.ActiveView.getViewer()
sg = viewer.getSceneGraph()
old_camera = Gui.ActiveDocument.ActiveView.getCameraNode()
self.camera = SoPerspectiveCamera()
# use current viewport camera pose as initial pose
self.camera.position.setValue(old_camera.position.getValue())
self.camera.orientation.setValue(old_camera.orientation.getValue())
self.camera.nearDistance.setValue(self.near_plane)
self.camera.farDistance.setValue(self.far_plane)
self.light = SoDirectionalLight()
self.root = SoSeparator()
self.root.ref()
self.root.addChild(self.light)
self.root.addChild(self.camera)
self.root.addChild(sg)
self.render_manager = SoRenderManager()
vp_reg = SbViewportRegion(self.w, self.h)
self.render_manager.setViewportRegion(vp_reg)
self.render_manager.setSceneGraph(self.root)
bbox_action = SoGetBoundingBoxAction(vp_reg)
bbox_action.apply(self.root)
bbox = bbox_action.getBoundingBox()
bbox_vol = bbox.getVolume()
if bbox_vol > 100:
self.size_mod = bbox_vol ** (1/3) # movement velocity will be proportional to cube root of scene volume
print (f"size mod: {self.size_mod}")
self.near_plane = self.size_mod / 100
self.far_plane = self.size_mod * 10
self.camera.nearDistance.setValue(self.near_plane)
self.camera.farDistance.setValue(self.far_plane)
while self._running:
frame_start = sdl3.SDL_GetTicks()
if self.frame_duration > 0:
self.r_rate_mod = self.frame_duration
# fps = 1000 / self.frame_duration
# print (f'Framerate is {fps:.2f} frames per second')
self.process_events()
time.sleep(0.02) # TODO do proper threading management, to avoid segfaults
self.draw()
self.frame_duration = sdl3.SDL_GetTicks() - frame_start
def terminate(self):
self._running = False
self.root.unref()
sdl3.SDL_GL_DestroyContext(self.context)
sdl3.SDL_DestroyWindow(self.window)
sdl3.SDL_Quit()
print('Terminated...')
if __name__ == "__main__":
sdltest = SDLTest()
t = Thread(target=sdltest.run)
t.start()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment