Wheeled quadruped simulator built with Genesis.
- randomized robot morphology and terrain
- photo-realistic rendering with dynamic shadows
- PID-stabilized suspension control
- predictive inverted-pendulum longitudinal control (command-aware stance IK + brake feedforward)
- Smartphone remote control via gdog-remote companion app with WebSocket and optional WebRTC 4 UDP
Clone repo:
git clone https://github.com/Felipegalind0/gdog-sim
cd gdog-simCreate .venv if it does not exist, activate it, and install dependencies:
python3 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install -r requirements.txtRun the sim with interactive viewer:
.venv/bin/python main.py --render --seed 829297643 --quick-tunnel
.venv/bin/python main.py --render --seed 829297643 --quick-tunnel --quick-tunnel-protocol http2 --quick-tunnel-edge-ip-version 4main.py: simulation entrypoint, scene setup, control loop, suspension PID + IK, wheel drive, HUDprocedural_gen.py: randomized robot URDF generation and randomized terrain/albedo generationgdog.urdf.jinja: robot template used to build a temporary URDF at runtimecamera_controller.py: follow camera, mouse/keyboard hooks, HUD overlays, in-view command inputcommands.py: command state container, remote payload parsing, suspension command parser/executornetwork.py: FastAPI app exposing/wsand optional/offerpid.py: PID controller with telemetry for debug overlaysmath_utils.py: quaternion helpers, leg IK helpers, camera math
On startup, main.py does the following:
- Parses CLI arguments (
--render,--video,--seed,--spawn-bone,--steps) - Initializes Genesis with GPU backend and a deterministic runtime seed
- Builds a lunar-looking scene:
- random terrain patch layout (center patch forced flat for stable spawn)
- moon-style grayscale albedo texture with crater-like variation
- directional sun-like light
- Generates a randomized robot from
gdog.urdf.jinja - Splits robot joints into leg joints (position-controlled) and wheel joints (velocity-controlled)
- Starts FastAPI control server in a background thread (
0.0.0.0:8000) - Runs the main simulation loop:
- consumes remote and keyboard commands
- computes speed-adaptive drive command envelopes
- predicts desired longitudinal acceleration from normalized command and speed error
- maps desired acceleration to desired tilt using
$\theta_{des} = \arctan\left(\frac{a_{des}}{g}\right)$ - converts desired tilt to fore-aft leg placement target and applies smoothed two-link IK
- scales brake authority with predictive deceleration demand before velocity ramp limiting
- applies roll/pitch stabilization through PID + two-link vertical IK
- computes skid-steer wheel targets (
left = vx - omega,right = vx + omega) - updates viewer/capture camera and overlays
The simulator now uses a command-predictive longitudinal controller instead of purely reactive stance shifting.
Control flow per simulation step:
- Read longitudinal command and normalize it to
[-1, 1]after drive envelope clamping. - Convert normalized command to desired forward speed target.
- Compute speed error against measured forward speed.
- Convert speed error to desired acceleration target and clamp to physical limits.
- Convert desired acceleration to desired pendulum tilt with
$\theta_{des} = \arctan\left(\frac{a_{des}}{g}\right)$ . - Convert desired tilt to desired leg X placement (inverted-pendulum wheel placement proxy).
- Filter and clamp leg X target, then apply IK for each leg.
- Compute predictive brake feedforward scale from deceleration alignment and apply it to brake ramp limit.
Sign convention used by stance IK:
- positive leg X: wheels move backward relative to body
- negative leg X: wheels move forward relative to body
This makes acceleration and deceleration one symmetric signal with opposite sign, while still allowing independent safety limits for traction and tip risk.
Interpretation: the controller estimates the non-gravity longitudinal force needed to balance an inverted pendulum via desired acceleration, then realizes that demand through wheel braking/drive authority plus stance placement.
Primary predictive stance and braking knobs in main.py:
STANCE_SHIFT_MAX_LEG_X_M: hard fore-aft IK travel capSTANCE_SHIFT_CMD_SPEED_MAX_MPS: maps normalized command to desired speed targetSTANCE_SHIFT_SPEED_ERROR_TO_ACCEL_GAIN: converts speed error to desired accelerationSTANCE_SHIFT_ACCEL_MAX_MPS2: clamp for desired acceleration magnitudeSTANCE_SHIFT_TILT_TO_LEG_X_GAIN: maps desired tilt to leg X shiftSTANCE_SHIFT_FILTER_ALPHA: stance target smoothing factorDRIVE_BRAKE_PREDICTIVE_GAIN: extra brake authority from predictive decel demand
Existing drive safety/ramp controls still apply on top:
DRIVE_ACCEL_RESPONSE_GAIN,DRIVE_BRAKE_RESPONSE_GAINDRIVE_VX_ACCEL_LIMIT[_STATIONARY],DRIVE_VX_DECEL_LIMIT[_STATIONARY]DRIVE_BRAKE_THROTTLE_MIN,DRIVE_BRAKE_REVERSE_SCALE
- Python 3.11
- A Python virtual environment (existing
.venvin this repo is preferred) - Upstream Genesis wheel support for your platform
Supported in this repo today:
- macOS arm64
- Linux x86_64
- Linux ARM64 (for example Ubuntu 24 on NVIDIA DGX Spark) via custom compile script
Install simulator dependencies:
source .venv/bin/activate
python -m pip install -r requirements.txtSince upstream Genesis binaries (quadrants, etc.) are missing for Linux ARM64, you must compile them from source.
Option 1: Build with Docker (Recommended for DGX/HPC clusters)
If you do not have sudo privileges on the machine, you can run everything inside a container:
docker build -t gdog-sim .
docker run --rm --ipc=host -p 8000:8000 --gpus all \
-e NVIDIA_DRIVER_CAPABILITIES=all \
-e DISPLAY=$DISPLAY \
-v /tmp/.X11-unix:/tmp/.X11-unix:rw \
gdog-sim python main.py --render --host 0.0.0.0Note on Ubuntu 24 & Wayland: Ubuntu 24 defaults to Wayland, even on modern NVIDIA drivers. If
echo $XDG_SESSION_TYPEreturnswayland, the X11 bridge above relies on Xwayland and might have hardware acceleration penalties. To pass Wayland natively, use:docker run --rm --ipc=host -p 8000:8000 --gpus all \ -e NVIDIA_DRIVER_CAPABILITIES=all \ -e WAYLAND_DISPLAY=$WAYLAND_DISPLAY \ -e XDG_RUNTIME_DIR=$XDG_RUNTIME_DIR \ -v $XDG_RUNTIME_DIR/$WAYLAND_DISPLAY:$XDG_RUNTIME_DIR/$WAYLAND_DISPLAY:rw \ gdog-sim python main.py --render --host 0.0.0.0
(If your cluster uses Apptainer/Singularity or Podman instead of Docker, the same Dockerfile will compile correctly).
Optional WebRTC support:
python -m pip install -r requirements-webrtc-optional.txtInteractive viewer (continuous until window close or Ctrl+C):
python main.py --renderDeterministic randomized world:
python main.py --render --seed 12345Deterministic randomized world with a random terrain bone prop:
python main.py --render --seed 12345 --spawn-boneCustom backend bind host/port:
python main.py --render --host 0.0.0.0 --port 8000Fixed-length run:
python main.py --render --steps 2000Unlimited loop explicitly:
python main.py --render --steps 0Video capture mode:
python main.py --videoVideo mode writes wheeled_go2.mp4 (50 FPS) when the run completes.
If you want to use the HTTPS GitHub Pages remote with your local simulator backend, run an ephemeral Cloudflare Quick Tunnel.
Install once (macOS):
brew install cloudflaredThen launch sim with tunnel:
python main.py --render --quick-tunnelIf your network is slow to provision the quick tunnel URL, increase wait time:
python main.py --render --quick-tunnel --quick-tunnel-timeout 60For restrictive guest/corporate Wi-Fi, force TCP-based edge transport and IPv4:
python main.py --render --quick-tunnel --quick-tunnel-protocol http2 --quick-tunnel-edge-ip-version 4On startup, the simulator prints:
- local backend targets (
<ip>:8000) - tunnel backend target (for example
https://random-name.trycloudflare.com) - a prefilled remote link (for example
https://felipegalind0.github.io/gdog-remote?backend=https://...) - a terminal QR code for that prefilled link
If terminal QR rendering dependency is missing, startup prints a fallback QR image URL instead.
Paste the printed tunnel URL into gdog-remote backend input when using GitHub Pages.
Notes:
- Cloudflare Quick Tunnel does not require an API key/token for this flow
- URL is temporary and usually changes each run
Useful options:
--remote-url <url>: override remote page URL (default is hardcoded tohttps://felipegalind0.github.io/gdog-remote)--no-qr: print link only, disable terminal QR rendering--quick-tunnel-timeout <seconds>: wait longer for tunnel URL discovery--quick-tunnel-attempts <n>: retry quick tunnel startup before failing--quick-tunnel-protocol auto|http2|quic: cloudflared edge transport; usehttp2on restrictive networks--quick-tunnel-edge-ip-version auto|4|6: force IP family; use4when guest Wi-Fi has broken IPv6
If quick tunnel URL discovery still times out in the simulator, run cloudflared manually in a second terminal:
cloudflared tunnel --url http://127.0.0.1:8000 --no-autoupdateThen copy the https://...trycloudflare.com URL from that terminal into gdog-remote backend input.
--stepsprovided: exact value is used--videowithout--steps: 500 steps--renderwithout--steps: unlimited- headless without
--steps: 10000 steps
The backend accepts JSON command payloads with:
vx: linear velocity commandomega: yaw-rate commandpitch_cmdorpitch: normalized pitch setpoint in[-1, 1]roll_cmdorroll: normalized roll setpoint in[-1, 1]cam_dx/dx,cam_dy/dy,cam_zoom/zoom: camera delta inputscmdorcommand: text command for suspension console parser
When arrow keys are held in render mode, keyboard commands override remote drive commands:
- Up/Down: forward/back
- Left/Right: yaw
- Camera follows robot XY and yaw automatically
- Mouse rotate drag controls orbit latitude
- Mouse wheel:
- horizontal scroll changes orbit longitude
- vertical scroll changes orbit latitude
- Shift + wheel zooms in/out
- Ctrl + wheel adjusts camera target height offset
/ort: open command inputEnter: submit commandEsc: cancel command inputh: toggle shadows
Supported control/tuning commands include:
help,statusrespawn,reset,pid_reset- pitch PID:
kp=...,ki=...,kd=...(also readable viakp?, etc.) - roll PID:
rp=...,ri=...,rd=...(also readable viarp?, etc.) - mix sign toggles:
p_sign=1|-1,r_sign=1|-1,p_sign=flip - suspension enable:
susp=on|off - debug toggles:
debug_pitch=on|off,debug_roll=on|off,debug_yaw=on|off,debug_speed=on|off
Unknown keys are reported with suggestions, and invalid values return per-key hints.
- Always-on status (top-center):
- suspension ON/OFF
- current pitch and roll (degrees)
- Optional debug blocks (top-right) controlled by debug flags:
- pitch PID internals
- roll PID internals
- yaw diagnostics
- speed/wheel command diagnostics
- Endpoint:
ws://<host>:8000/ws - Behavior: accepts text JSON frames and updates command state
- Endpoint:
POST http://<host>:8000/command - Behavior: accepts JSON command payloads using the same fields as WebSocket frames
- Purpose: lets control commands pass through restrictive networks that block WebSocket upgrades
- Endpoint:
GET http://<host>:8000/capabilities - Returns JSON such as
{"webrtc": false}
- Endpoint:
POST http://<host>:8000/offer - Requires
aiortcinstalled - If
aiortcis missing, endpoint returns:
{"error":"WebRTC not installed"}Voice command progress/result events stream over WebSocket or WebRTC data channel. If the remote is in HTTP fallback mode, basic joystick control still works, but voice command telemetry will be limited.
Start simulator and remote UI in separate terminals.
- In this repo:
source .venv/bin/activate
python main.py --render- In sibling repo
../gdog-remote:
npm install
npm run dev- Open the Vite URL (typically
http://localhost:5173)
The remote app streams controls at 50 Hz and prefers WebRTC data channel when connected; otherwise it sends over WebSocket.
requirements.txt: aggregate install (runtime + Genesis platform marker)requirements-runtime.txt: FastAPI/transport + direct Python deps used by this reporequirements-genesis.txt: Genesis package (skips Linux ARM64)requirements-genesis-deps.txt: legacy explicit Genesis transitive set (not used by default install)requirements-webrtc-optional.txt: optionalaiortc
aiortc not installed. WebRTC disabled. Using WebSockets as primary.- Expected unless optional WebRTC dependency is installed
- Remote shows capability probe warning
- backend may be unreachable, blocked by mixed-content rules, or blocked by network policy
/capabilitiesshould exist; test withcurl http://<host>:8000/capabilities- if WebSocket is blocked on guest Wi-Fi, remote should automatically switch to HTTP fallback mode
- Cloudflare quick tunnel exits with
status_code="500 Internal Server Error"anderror code: 1101- quick tunnel API is failing upstream (not a simulator bug)
- retry after a minute or use an alternate egress network/VPN
- consider a named Cloudflare tunnel (account-backed) for higher reliability
- Robot does not react to remote controls
- confirm sim is running
- confirm backend reachable at
http://localhost:8000/ws - verify remote status shows WebSocket connected
- Startup/import issues
- activate
.venv - reinstall with
python -m pip install -r requirements.txt
- activate
- Linux ARM64 startup exits with Genesis unavailable message
- Run
./scripts/install_ubuntu_arm64.shto compile the missing binaries.
- Run
