A ready-to-fork template for building 3D games with Gaussian splat environments, a kinematic character controller, and real-time physics.
Stack
| Layer | Library |
|---|---|
| Renderer | Three.js r180 |
| 3D World | World Labs Marble |
| Gaussian splats | @sparkjsdev/spark 2.0 with LoD |
| Physics | @dimforge/rapier3d-compat 0.12 — KCC + dynamic rigid bodies |
| Post-processing | pmndrs/postprocessing — bloom, vignette, brightness/contrast |
| GUI | lil-gui |
| Build | Vite 6 + TypeScript |
Controls
| Key | Action |
|---|---|
| W / A / S / D | Move |
| Shift | Sprint |
| Space | Jump |
| 1 | Trick animation |
| Right-click drag | Orbit camera |
| Scroll | Zoom |
- Node.js 18.18+ (20 LTS recommended)
- npm 9+
npm ci # clean, lockfile-exact install
npm run dev # dev server → http://localhost:5173npm run build
npm run previewnpm run verify # TypeScript + production build checkOpen src/main.ts and find the WORLD_ASSETS block near the top:
const WORLD_ASSETS = {
splatSpz: '/attic.spz', // ← your .spz Gaussian splat file
colliderGlb: '/collider.glb', // ← invisible collision mesh
colliderGlbUniformScale: 5, // ← scale to match splat units
} as const;- Drop your
.spzand collider.glbintopublic/. - Update the paths and scale.
- The rest of the scene, character, and physics adapt automatically.
Tip: Export the collider as a simple, low-poly mesh from Blender. The code bakes it into a Rapier trimesh at startup.
Find the CHARACTER_ASSETS block right below WORLD_ASSETS:
const CHARACTER_ASSETS = {
glb: '/dog.glb', // ← your animated character GLB
} as const;Then update the clip name constants to match your model's animation names:
const CLIP_HAPPY_IDLE = 'Happy Idle'; // ground idle
const CLIP_AIR_IDLE = 'Idle'; // airborne idle
const CLIP_WALK = 'Brutal To'; // walk / run
const CLIP_TRICK = 'Step Hip Hop'; // press-1 emote (optional)The character system auto-scales the model to match the capsule height and handles idle → walk → air crossfades.
All gameplay values live in the tuning object and are exposed live in the GUI panel (top-right corner). Key parameters:
tuning key |
What it does |
|---|---|
moveSpeed |
Base movement speed (m/s) |
jumpSpeed |
Jump launch velocity |
gravityY |
World gravity (negative = down) |
characterHeight |
Auto-scales the mesh to this height |
characterTurnSpeed |
How fast the character yaws toward input |
splatUniformScale |
Global scale of the splat world |
Changes in the GUI update in real time — useful for dialing in feel without restarting.
character-controller-final/
├── public/ # Runtime assets (served at /)
│ ├── attic.spz # Gaussian splat world
│ ├── collider.glb # Invisible physics mesh for the world
│ ├── dog.glb # Animated character
│ └── pano.jpg # Panorama for character/ball reflections
├── src/
│ └── main.ts # Entire app — scene, physics, KCC, GUI
├── index.html
├── vite.config.ts
├── tsconfig.json
└── package.json
Why one file?
src/main.tsis intentionally self-contained so the flow is easy to read top-to-bottom. Split it as you see fit for your own project.
Generate a zip (excludes node_modules and dist):
npm run package:zip
# → ../character-controller-share.zipCustom filename:
npm run package:zip -- my-game-demo
# → ../my-game-demo.zipRecipient steps after unzip:
rm -rf node_modules dist # if replacing an older copy
npm ci
npm run dev- Three.js is pinned to
0.180.0— do not bump without testing. Later releases can break@sparkjsdev/sparktypings or behavior. package-lock.jsonis committed sonpm cialways installs the exact tested versions.
MIT — fork freely and build something fun.