Materializr
오픈소스 파라메트릭 3D CAD로 제약 스케치·솔리드 모델·나사·SVG·텍스트 조각·STL/STEP 내보내기를 지원합니다
A desktop 3D game editor — built on Three.js, React and Electron
저장소 README에서 가져옴
README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
A desktop 3D game editor — built on Three.js, React and Electron.
Three Studio is a scene editor and game runtime for the web, running as a native desktop app.
You open a project, drag models into a viewport, place lights, cameras, colliders and sounds,
attach TypeScript behaviours, press Play to test inside the editor — then package the whole
thing as a static web build that runs in any browser.README 전문 보기 ↗A desktop 3D game editor — built on Three.js, React and Electron.
Three Studio is a scene editor and game runtime for the web, running as a native desktop app. You open a project, drag models into a viewport, place lights, cameras, colliders and sounds, attach TypeScript behaviours, press Play to test inside the editor — then package the whole thing as a static web build that runs in any browser.
This is a proof of concept. It is a personal R&D project: it runs, it edits, it plays, it exports — but it is not production software. Expect rough edges, missing features, breaking changes to on-disk formats between commits. Installers are published on the Releases page but are unsigned, so macOS and Windows will both warn before letting them run. There is no support commitment and no stability guarantee.
What it is: a serious, tested exploration of how much of a real game editor can live on top of Three.js and the web platform. 705 unit tests, strict package boundaries, and a headless smoke harness that boots the whole stack.
RMB + WASD), orbit, pan, dolly, frame selection.mesh · model · water · camera · light · collider · rigidbody · script ·
audioSource · audioListener · playerController · prefabInstance · material assets
The Inspector is generated from a component's declared schema (Tweakpane), including asset pickers and entity references.
.meta.json sidecars: scenes reference ids, never paths, so folders and assets
can be renamed freely.time node is re-pointed at it, so
Pause and the timescale stop the water and the sky along with the physics.onAwake / onStart / onUpdate / onFixedUpdate / onLateUpdate / onDestroy), declared
editable properties (numbers, colours, vec3, enums, entity and asset pickers), and APIs for
input, scenes, timers and audio.spatialBlend dial, buses, per-window editor/game isolation./, /games/demo/, http://localhost:8080/) and the exporter writes a
<base> on the page, which moves the player bundle, the JSON, the assets and the scripts
together. It is also the fix for a host that serves /games/demo without redirecting to
/games/demo/, where relative URLs resolve one level too high.| Node.js | >= 22.12 — repo is developed on 24.8.0, see .nvmrc |
| npm | 10+ (workspaces) |
git clone https://github.com/three-studio/three-studio.git
cd three-studio
nvm use # Node 24.8.0
npm install # workspaces + patch-package postinstall
Error: Electron uninstallon first run? The Electron binary was not fetched by the postinstall hook. Get it manually:node node_modules/electron/install.js
npm run dev
This builds the web player once, starts electron-vite and opens the launcher. Create or pick a project folder — the editor window opens on it, with renderer HMR live.
| Command | What it does |
|---|---|
npm run dev | electron-vite dev server + Electron, renderer HMR |
npm run build | Production build into apps/desktop/out |
npm run dist | Build + package for this machine into apps/desktop/release |
npm run dist:mac / dist:win / dist:linux | Same, targeting one platform explicitly |
npm run build:web | Builds the exported-game player into apps/web-template/dist, and the device client into apps/web-template/dist-device |
npm run build:packages | Builds the three library packages for npm into packages/*/dist |
npm run typecheck | tsc --noEmit across every workspace |
npm test | Vitest (includes the package-boundary assertions) |
npm run test:watch | Vitest in watch mode |
npm run dist packages for the machine it runs on. The per-platform variants exist for CI, and only
work there: esbuild is a runtime dependency — the packaged app compiles user scripts with it —
and npm installs only the current machine's binary, so npm run dist:win from a Mac produces an
installer that launches and then cannot compile a single script, with nothing failing along the way.
Releases therefore go through GitHub Actions, one native runner per target. One manual workflow in the
Actions tab — Release (.github/workflows/release.yml) — and nothing fires on a push.
Pick the branch under Use workflow from, type a tag name, run it. Any name git can store is allowed:
v1.4.0, v1.4.0-rc.1, v2026-summer. The run refuses a name git cannot store and one already taken,
then runs the typecheck and the suite at that commit, builds macOS arm64 + x64, Windows x64 and Linux
x64 on four native runners, tags the commit, and publishes a release with the installers attached —
marked Pre-release when the version carries a -.
Two things about that order are deliberate. The tag is created last, once all four installers
exist: a run that fails leaves the name free to try again rather than burning it on a commit that never
shipped. And the run is dispatched from a branch, because workflow_dispatch executes the workflow
file as it exists on the ref you pick — dispatch from a tag and you get the file that tag was cut with,
so a fix to the release process could never apply to the release that needed it.
The tag is the authority on the installer: the release run stamps it into
apps/desktop/package.json on the runner, the one file electron-builder reads. v1.4.0 therefore
gives installers named three-studio-1.4.0-… and an app that declares 1.4.0 to the operating
system, with nothing to bump by hand for a release. Nothing is committed — the version in the
repository only serves as the base for a tag that is not a version number, which packages as a
prerelease of it (v2026-summer → 0.6.0-2026-summer).
What the app displays is a different number, on purpose. The launcher and the status bar read
ENGINE_VERSION from packages/core/src/constants.ts, which no
release touches — it also keys the derived indexes and stamps project.engineVersion, so it is the
source tree's own identity rather than the installer's. Bump it in a commit when the product moves;
the two agree whenever a tag is cut to match a bump, which is the ordinary flow. The note on the
constant is the full reasoning.
A blank window and a working window look identical in a build log, so the stack can boot itself and report what the renderer actually painted:
STUDIO_SMOKE=1 npm run dev
STUDIO_SMOKE=1 STUDIO_SMOKE_SHOT=/tmp/shot.png npm run dev
Four more environment variables steer it, all read in
apps/desktop/src/main/smoke.ts:
STUDIO_SMOKE_SHOT=<path> | Also write a PNG of what was painted |
STUDIO_SMOKE_PROJECT=<path> | Open this project instead of stopping at the launcher |
STUDIO_SMOKE_SETUP=<js> | Run this in the renderer first, to drive the editor into the state worth looking at |
STUDIO_SMOKE_SETTLE=<ms> | Wait longer than the default 800 ms after the setup before capturing |
| Input | Action |
|---|---|
| Right button held | Mouse look; WASD to fly, Q/E down/up, Shift to boost |
| Wheel while looking | Adjust fly speed |
| Wheel | Dolly |
| Middle drag | Pan |
Alt + left drag | Orbit the pivot |
F | Frame the selection |
Q W E R | Select / Move / Rotate / Scale |
Cmd/Ctrl + Z / Shift+Z | Undo / redo |
Cmd/Ctrl + D | Duplicate |
Delete | Delete selection |
packages/core @three-studio/core — Pure data: scene schema, project schema, serialization, import contracts.
Depends on nothing — not three, not react, not node.
packages/runtime @three-studio/runtime — The game engine: three.js + Rapier + audio + script host.
Never imports the editor.
packages/editor @three-studio/editor — Editor UI: React shell, dock layout, viewport tooling, inspector, commands.
apps/desktop Electron main / preload / renderer, import sessions, packaging.
apps/web-template The player an exported game ships with.
Inside the monorepo the three packages are consumed as TypeScript source: Vite, electron-vite and
Vitest all alias @three-studio/* onto packages/*/src, mirroring the paths in
tsconfig.base.json. Nothing here reads a build.
They do have one, and it exists only for npm — npm run build:packages writes an ESM bundle and a
declaration tree into each packages/*/dist, which is what npm publish ships. It runs core →
runtime → editor, in that order: the build configs clear the root paths, so each package resolves
its siblings the way a consumer does.
Two Vitest assertions in packages/runtime/test/package-boundary.test.ts keep core
dependency-free and runtime editor-free.
It's a POC, so the bar is simple: npm run typecheck and npm test both green, and a test that
would have caught the bug you fixed. Formats on disk are versioned — never remove a field, migrate
through the type's own factory, and leave unknown data untouched.
MIT. The three library packages — @three-studio/core,
@three-studio/runtime and @three-studio/editor — are
published under the @three-studio npm scope as ESM with type declarations. three, react and
react-dom are peer dependencies, and a consumer has to alias three to three/webgpu: three's own
addons import the bare specifier, and a second copy of three makes every instanceof across the
boundary fail. Each package's README says what else it expects.