Vulkan 预设需要 Vulkan SDK 在 PATH 中或启用 vcpkg 的 vulkan feature;Assimp、ImGui、PhysX、V-HACD 等头文件需额外依赖才能使用;用 FetchContent 引入时不会下载 threepp_data 示例资源。
threepp
markarenMIT活跃维护用 C++20 实现、带 Python 绑定的跨平台 3D 图形库,API 风格对齐 three.js,方便在原生和 Web 环境复用同一套高层接口做渲染与物理仿真。
A cross-platform C++20 3D library with Python bindings and the high-level API of three.js.
cmake --preset gl
cmake --build --preset gl
ctest --preset glREADME 未给出具体显存或 GPU 型号要求;Vulkan 后端依赖光追扩展(ray query 与加速结构),不支持时退化为光栅渲染,macOS 上 MoltenVK 无光追,保守选择是 GLRenderer。
用 three.js 的 API 写 C++20 3D 程序,还自带 Vulkan 光追和仿真传感器
用 C++20 实现、带 Python 绑定的跨平台 3D 图形库,API 风格对齐 three.js,方便在原生和 Web 环境复用同一套高层接口做渲染与物理仿真。
核心能力
- API 对齐 three.js r129,OpenGL 路径补到 r186 特性
- 同一场景图下可切换 OpenGL 与延迟 Vulkan 两个后端
- Vulkan 后端支持光追阴影、反射、AO、GI 与 DLSS/FSR
- 内置 PhysX 物理、URDF 关节与多种仿真传感器
- 提供 Python 绑定并发布到 PyPI,可 pip 安装
容易踩的坑
- 2026-11-04最新正式版2026-11-04 · 2026-11-04 19:32 UTC
The short version: DLSS Ray Reconstruction is the Vulkan denoiser wherever DLSS runs (one stochastic ray per denoised term, a quarter of the shade's rays), the sun and the lamps cast shadows from shadow maps, on a GPU without ray tracing and by choice on one with it, a streamed world edits the frame instead of rebuilding it (a mesh added, removed, shown, hidden or re-skinned is a slot edit; a cell can arrive hidden and be admitted over the frames after), the Vulkan frame's CPU work runs on the thread pool (secondary views, shadow walks, TLAS rows, the cull in blocks), USD robots load as PhysX articulations with their mimic couplings, a rotor's downwash ripples the water, bends the grass and blows a hollow in the dust, displacement maps work on Vulkan, OpenGL gets a planar mirror and the Vulkan post stack's white balance and colour grade, and Python gets input callbacks, a Raycaster, skinned meshes, batched transform and vehicle setters, headless capture and comparison, and the USD importer. The Windows wheels carry the FSR 3.1 upscaler.
Figures below come from the commits that measured them, or from the measurements that set a default, on the hardware named there: an RTX 4070 until 2026-10-21 and an RTX 5080 after it, an RTX 4060 Laptop GPU, and a Ryzen 9 5950X for the CPU figures. They were not re-benchmarked for this release.
⚠️ Breaking and behaviour changes
API
FBXLoaderis removed, withTHREEPP_WITH_FBX, the OpenFBX fetch, the Python binding and thefbx_loaderexample.scripts/fbx_to_gltf.pyconverts an FBX with Blender and rebuilds its materials with the rules the loader used (ORM specular to metallicRoughness, glass to transmission and ior, foliage to MASK, emissive strength). Textures are referenced in place, DDS throughMSFT_texture_dds, whichGLTFLoadernow reads.- RLtools is removed: the
rltools_pendulumandrltools_swarmdemos, the fetch,THREEPP_WITH_RLTOOLSand the third-party entry. TypedBufferAttribute::copyAt,copyArray,copyColorsArrayandcopyVector{2,3,4}sArrayare gone. Nothing called them.Renderer::gammaFactoris gone, with theGAMMA_FACTORdefine no shader read, and so is thelightmap_fragmentshader chunk.ImguiContext::makeDpiAware()(a deprecated stub) is gone, andImguiContext.hppno longer includesMonitor.hpp: include it yourself where you use the monitor namespace.UrbanProps::lotBayPitch,lotRowPitchandlotOccupancyare gone (read nowhere).Articulation::addLinkthrows on a joint type it does not know. It used to build a free hinge for"fix","Fixed"or""."fixed"is now a joint type of its own: a link with its own collider and mass and no degree of freedom, so it takes no place injoint_positionsorset_drive_targets.addLinkalso takes a PhysX drive type as a trailing argument.- Python
GLRenderer.read_pixels(deferred=True)returnsNoneon the first call (the frame queued the call before);flush_pixels()drains the ring. The synchronous call is unchanged. - Python
VulkanRenderer.render_scaleraisesValueErroroutside [0.25, 1.0] instead of clamping. - Python
Flock.set_perch_filtertakes any callable (the predicate is wrapped;Noneclears it). It used to copy a non-copyable mesh and raise at bake. - Python: a node looked up in a tree keeps only itself alive.
get_object_by_name,.parentand the lights'get_targetno longer tie the result toselffor a shared-owned node, so a clone and its nodes looked up from each other can die (the dune-buggy's cloned cars never did: about 80 MB of GPU memory and 250 MB of RAM per race of five). A node nothing shares (a light's built-in default target) is still kept by its owner. - The
THREEPP_*environment knobs of the Vulkan renderer read one way (EnvKnob.hpp): a flag is on for any value that does not start with0, an integer or number falls back to the
- 2026-10-112026-10-11 · 2026-10-11 10:19 UTC
查看更新说明
The short version: the Vulkan ocean keeps what boats leave on it (a wake field of foam, bubbles and the waves a hull makes, for up to eight vessels on one sea), Python gets a set of vehicle rigs (three sea drones, an AUV, five aircraft and a car, each built from one spec file with its sources cited), text follows the font's real curves and is UTF-8, a Vulkan process exits in milliseconds instead of 20 seconds, the Vulkan renderer runs on a GPU without ray tracing, and the web build is lit by its environment and plays sound. Three new OpenGL demos load no assets at all:
showpiece,MarbleRunandSandTable.Figures below come from the commits that measured them, on the hardware those commits name. They were not re-benchmarked for this release.
⚠️ Breaking and behaviour changes
API
type()returnsconst std::string&onObject3D,Material,BufferGeometryand everything under them. It returned astd::stringby value, which cost a heap allocation per call for the longer names, several times per draw. Callers compile unchanged. A class of your own that overridestype()must take the new return type.Font::glyphsis keyed by Unicode code point, not bychar.- Removed from
VulkanRenderer:setDeferredDenoise/deferredDenoise(usesetDenoise/denoise),setVolumetricFog(a no-op since the fog unification; Python'svolumetric_foggoes with it), and theVulkanRendererCore.hppheader and alias. FireEffect::Params::legacyEmbersandemberRateare gone, withvulkan_fire --legacy-embers. The embers are aParticleField.PhysxVehicle:Settings::stickyDampingLongitudinalandstickyDampingLateralare gone. The vehicle now chooses betweenstickyDampingandstickyDampingHoldeach step (see the grade hold below).Texture::premultiplyAlphais removed. Neither renderer read it. The key in an old scene file is ignored on load.Material::clippingis gone from the base class. It shadowedMaterialWithClipping::clipping, which is the one that remains.PhysxSoftBody::registerGlTexturereturnsbool, likeregisterVulkanMemory.THREEPP_DLSS_JITTER_SIGNis removed. The per-axisTHREEPP_DLSS_JITTER_SIGN_X/_Ystay.- ABI: many classes changed layout. Rebuild everything that links threepp.
Behaviour
Canvas::Parameters::fastExitis on by default. A process on its way out leaves its Vulkan device to the OS instead of destroying it, which on NVIDIA drivers could wait about 20 s invkDestroyDevice. An app that tears its renderer down and builds a new one while the process goes on setsfastExit(false). Python'sCanvas(fast_exit=...)keeps itsFalsedefault, since interpreter exit is covered automatically.VulkanRendereris created on a device without ray tracing. It used to throw there. It now draws a degraded frame (see the no-ray-tracing mode under New). Code that caught the exception to fall back toGLRendererasksrayTracingSupported()(Pythonray_tracing) instead. Where there are several devices, one with ray tracing is still preferred.- A Vulkan readback inside the frame returns that frame.
readRGBPixels(),writeFramebuffer()and Python'ssave_frameused to copy a swapchain image up to three frames old, so a script that changed the scene and saved a still got the old scene. The readback now ends the open frame first. An ortho HUD pass that arrives after the readback in the same iteration is skipped with a one-time hint: put it before the readback. Baselines captured through this path change once. - Python:
VulkanRendererdraws anOrthographicCameraas a 3D view (orthographic_scene_rendering, on by default). A top-down render used to come out as unlit fills in scene order with a black background. The C++ default is unchanged, but a lit mesh sent down the 2D path now prints a one-time hint that names the call to mak
- 2026-09-282026-09-27 · 2026-09-27 21:10 UTC
查看更新说明
Fixes
-
MinGW shared Debug builds link again. At
2026-09-27, a MinGW-w64 GCC Debug build withBUILD_SHARED_LIBS=ONfailed to linklibthreepp.dllwithrelocation truncated to fit: IMAGE_REL_AMD64_SECREL. The same happened when threepp was pulled in withFetchContent.ObjectLoader.cppcompiled to 33,914 COFF sections, which is over the 32,767 a regular object file can hold.ObjectLoader.cppis now split in two.ObjectLoaderResources.cppbuilds the images, textures, materials, geometries and animations, andObjectLoader.cppbuilds the object tree from them. Each half is about 20k sections.GLTFLoader.cppwas next in line at 25,498 sections, so it is now split into three files:GLTFLoader.cpp,gltf/GLTFMaterials.cppandgltf/GLTFMeshes.cpp. The largest of these is about 16k sections.- As a safeguard, MinGW GCC now writes big-object COFF files (
-Wa,-mbig-obj), the MinGW equivalent of MSVC's/bigobj. A file that grows past the limit later won't break the build.
No behaviour changes: the code was moved, not rewritten.
Build
threepp_editor_coreis only built when something needs it. Projects that consume threepp throughFetchContentoradd_subdirectoryno longer compile its 37 sources. The editor, the player, the tests and the Python bindings still build it as a dependency. It was never part of the install.
CI
- New
windows-mingwjob. It builds threepp as a Debug DLL with MinGW-w64 GCC from MSYS2 and runs the test suite against it. Before this, no CI job built with GCC on Windows or built the shared library at all, which is why the link failure went unnoticed. The job also logs the objects with the most COFF sections, so file growth stays visible now that-mbig-objkeeps the build green.
-
- 2026-09-272026-09-27 · 2026-09-27 18:53 UTC
查看更新说明
The short version: the OpenGL backend catches up with three.js r186 (shadows, environment lighting, tone mapping, transparency, GTAO and outline passes), both backends now return the light a physically based material should, the Vulkan frame is one render graph that derives its own barriers and shares its scratch memory, and granular soil can carry a wheel, a rover or a whole car.
Figures below come from the commits that measured them, on the hardware those commits name. They were not re-benchmarked for this release.
⚠️ Breaking and behaviour changes
Read this before upgrading. A few items break at compile time, and many scenes will look different on both backends, mostly because energy that used to be lost or double-counted no longer is.
API
Texture::texCoordis replaced byTexture::channel(the three.js r152 name): 0 =uv, 1 =uv2, 2 =uv3. Nothing ever readtexCoord; the glTF loader now routestexCoordandKHR_texture_transform's override intochannel. AnaoMaporlightMapleft at channel 0 still samplesuv2when the geometry has one.ShaderChunk::lights_lambert_vertex()is gone. Lambert is lit per fragment now (lights_lambert_pars_fragment/lights_lambert_fragment). CustomShaderMaterials that included the vertex chunk need the fragment pair.- The Vulkan
IdsG-buffer AOV is 4× uint32, 16 bytes per pixel (was 4× uint16, 8 bytes). This applies toreadGBufferAOV/readViewGBufferAOVsand theIdsframe-interop channel. Entry indices used to wrap past 65,535, so entry 66,000 was shaded with entry 464's material. Stable ids are now 32-bit, capped at 2³¹−1 to matchLidarReturn::hitInstanceId. Python decoders read uint32, andcuda_interopexports ids as'<u4', falling back to an int32 view where torch rejects uint32. VulkanRendererrefuses a device withoutVK_KHR_ray_queryand throws with the device name. It used to dereference a null shade pass. Some datacenter drivers expose the other KHR ray-tracing extensions but not ray query; useEglContext+GLRendererthere (see below).Renderer::checkShaderErrorsdefaults totrue, as in three.js. A shader that fails to compile or link is now reported with the driver log instead of silently drawing nothing. Healthy scenes print nothing.- The env-sun policy (
EnvSunPolicy,envSunFound(),envSunDirection(),envSunColor()) moved to theRendererbase class.VulkanRenderer::EnvSunPolicyis an alias, so existing spellings compile. - ABI:
Texture,Material,BufferGeometry,RenderTargetandInstancedMeshlose a private member,Scenegains five fields, andLightShadow::copyis a new virtual. Rebuild everything that links threepp.
OpenGL: scenes look different
- Tone mapping happens once, on the way to the screen (three.js r154+). Anything rendered into a render target is now scene-linear HDR.
EffectComposerappends an implicitOutputPass(new) that tone-maps and encodes at the end, unless your chain already has one. Bloom thresholds real light now: an emitter at 10 blooms far more than one at 1.Reflector,Waterand the transmission capture render to half-float targets, so mirrors are no longer washed out and glass no longer tone-maps twice. A target that is the picture can opt back in withRenderTarget::displayTarget;CameraSensordoes, so GL sensor captures are byte-identical to before. - One sun, as on Vulkan. An equirect HDR with a sun baked in used to light every diffuse surface on top of the scene's own
DirectionalLight. GL now clamps the disc out of the rough PMREM levels and re-adds the removed energy as one shadowless analytic light, but only while the scene has no visibleDirectionalLight. HDRI-lit GL scenes get darker and cooler. On the forest demo, luma went from 77 to 48, close to Vulkan. - Physically based energy, per three.js r185:
- Rough metals under an environm
- 2026-09-062026-09-06 · 2026-09-06 15:10 UTC
查看更新说明
New this release: imaging sonar, ray-traced AO with bent normals, zero-copy frame export to CUDA, splat point clouds and splat scene serialization, PhysX tendons, and the
GeoSceneterrain stack.Breaking changes
Object3D::lookAt()now applies the camera convention (local-Ztoward the target) to sensors. New virtualObject3D::usesCameraLookAtConvention()(defaultfalse) is overridden byTracedRasterVisionSensor(soDepthSensorandLidarSensor),PathTracedLidarSensor,CameraSensorandSonarSensor. Code that compensated with the mirror pointlookAt(2*position - target)now aims backwards and must pass the target itself. Adding a virtual toObject3Dchanges its vtable layout.Imudefaults are the ICM-42688-P datasheet figures:gyroNoise0.005 → 4.8869e-5 rad/s/√Hz,accelNoise0.06 → 6.8647e-4 m/s²/√Hz, both random-walk terms → 0. Seeds are unchanged, so seeded replays keep their streams, but existing scenes get different noise magnitudes.LidarModel::OS1_64()andOS0_128()step azimuth at 360/1024° (0.3515625) instead of 0.35°: 1024 columns per revolution instead of 1029.BVH::intersects()andBVH::intersect(..., accurate=true)run an exact triangle-triangle test instead of the triangle-AABB overlap approximation. Near-miss pairs that previously reported a hit no longer do.BVH::build(positions, indices)throws on malformed input, andgetGeometry()returnsnullptrafter the array build.ArticulationLink's constructor gained a leadingconst PhysxWorld*parameter.PhysxGpuBatch::blockFloatsWritethrowsstd::runtime_errorfor unhandled write types (including the four tendon types) instead of returningmaxDofs.ParticleField::submitgainedfloat dtSec = 0.f, andVulkanRenderer::enableVertexInteropgainedbool stableCorrespondence = true. Both source-compatible; both ABI-breaking.RoadNetwork::buildBridgeMeshes()andbuildGroundChunkMeshes()keep their signatures but ignoretexWidth/texHeight. Resolution, tile length, UVs, vertex/index counts and materials now come fromMarkingRules; materials are shared per (marking class, surface, width, wear, variant), so mutating one returned mesh's material changes every piece of that class; ground chunks gain a child patch-decal mesh.buildMeshes()keeps the legacy path.buildGeoBuildingMeshes()emits up to nine named child meshes (geo_buildings_wood,_masonry,_industrial,_roof_slate, …) instead of the threegeo_buildings_walls,_walls_plain,_roof. Lookups by name now fail at runtime rather than at compile time.terrain::detail::geoBldRoofPalette()was removed fromGeoBuildings.hpp. Buildings with no OSM colour tag render in different colours.makeNorwegianSplat()emits eight layers instead of six (new shallow and seabed layers; wetlandheightLomoved from sea−5 m to sea−0.5 m). Terrain colour below the waterline changes for existing geo scenes.makeGeoProvider()tints terrain albedo from land cover by default:GeoTerrainOptions::landCoverdefaults totrueandpack.flowis always computed, so wet-rock and scree tinting apply to every existing pack.loadSogWithLodthrows when a multi-level SOG asset's tree leaves do not tile a chunk exactly.- Rendering output changes: the ocean fine-cascade normal runs at full slope (an undocumented 0.5 factor was removed);
setDeferredAO(true)now selects the half-res RT AO + bent-normal pass; TAA history invalidation changes frame-0 output; the foam accumulator is capped at 1024 (THREEPP_OCEAN_FOAM_RES=2048restores the old density); the GLfogColoruniform is encoded at upload.
Added
Sensors
SonarSensor(new headerthreepp/helpers/SonarSensor.hpp) — Vulkan-only forward-looking imaging sonar, traced through the renderer's acceleration structure as one dispatch. Synchronousscan()plus the pipelinedscanBegin/scanFired/scanReady/scanCollect.SonarModel(new head
用户点评
登录用户的真实使用记录,可按版本和硬件筛选还没有人点评。第一个说出真实体验的人最有价值。
threepp
A cross-platform C++20 3D library with the high-level API of three.js, and two backends behind one scene graph: portable OpenGL, and a deferred Vulkan renderer with ray-traced shadows, reflections, AO and GI.
On top of that: a scene editor, PhysX robot simulation, simulated sensors with ground-truth labels for synthetic data, hardware-in-the-loop flight with ArduPilot, and Python bindings on PyPI.
An Otter X sea drone under way in a digital twin of a real waterfront in Ålesund, Norway, modelled
from Kartverket lidar and sea charts and rendered by the Vulkan backend: the water reflects the
moored boats and the buildings ashore (the scene is not in the repository)
New here? Getting started walks through the concepts the library is built from: the scene graph, ownership and lifetimes, geometry and materials, lights, the frame loop, loaders and the two backends. The examples folder is the de-facto reference.
Features
three.js parity. The API follows three.js r129, with selected features from r186 on the OpenGL path (shadow filtering, environment and background controls, material fixes, the newer post-processing passes).
- Mesh, InstancedMesh, Line, Points, Sprites, 2D/3D text (typeface.json and TTF)
- The usual geometries, lights (including RectArea), cameras, raycasting, animation, morph targets and bones
- Physical materials with transmission, alpha hash and alpha-to-coverage; PMREM environment maps and
RoomEnvironment - Shadows, Water and Sky shaders, and Orbit, Trackball, Fly, Drag and Transform controls
EffectComposerwith bloom, depth of field, GTAO, outline, FXAA and output passes- Loaders: glTF/GLB (with meshopt), OBJ/MTL, STL, COLLADA, SVG, URDF/xacro (no ROS needed), PNG/JPEG, DDS, WebP, HDR and EXR; opt-in USD and Assimp
- Scene serialization as three.js Object JSON or a single-file
.tpzarchive; three.js editor documents load as-is
Rendering beyond three.js
- A deferred Vulkan renderer: raster G-buffer, ray-traced shadows (or cascaded shadow maps), reflections, AO and probe GI, volumetric fog, denoising and TAA, DLSS and FSR 3.1 upscaling, automatic mesh LOD and GPU occlusion culling
Ocean(Vulkan): three-cascade FFT water with foam, wind, JONSWAP fetch and an underwater viewParticleField(Vulkan): GPU particles for fire, smoke, snow, rain and physics-coupled grains, drawn as meshes or billboards, or marched as participating media- Gaussian splatting:
SplatCloudrenders.plyand SOG scans on both backends, and a scan can be baked into a triangle surface for physics and sensors (doc/vulkan_splats.md)
Simulation and perception
- Ground-truth labels from the G-buffer (Vulkan): metric depth, stable instance ids, semantic
classes, normals and motion;
addViewrenders a whole camera rig from one scene build - Simulated sensors: lidar (VLP-16, HDL-32E, OS1-64, OS0-128 patterns), imaging sonar (Vulkan),
depth, colour and event cameras, IMU, joint encoders, contact and force/torque. Seeded and
sim-clock stamped: drive
setSimTimefrom the same clock and a frame replays bit for bit - A physical camera: exposure from aperture, shutter and ISO, OpenCV intrinsics, lens distortion applied to both image and labels, and sensor noise
- PhysX: rigid bodies, articulations built from URDF/xacro with PD drives, tendons and cables, soft bodies, GPU particles, vehicles, character controllers and V-HACD convex decomposition, plus a damped-least-squares IK solver
- Soft and granular ground from Python on NVIDIA Warp: Bekker-Wong mud and snow, and MLS-MPM grains that carry a wheel, checked against terramechanics data
- Hardware-in-the-loop: an unmodified ArduPilot flies a threepp airframe in lock-step (doc/ardupilot_sitl.md)
- Zero-copy CUDA interop: an external producer such as NVIDIA Warp writes vertex buffers in place, and rendered frames come back as CUDA tensors
Content and tooling
- Procedural terrain, trees, grass, roads, conveyors, bird flocks and a log cabin; real-world
terrain built from Norwegian open data (
terrain::GeoScene) - Audio via miniaudio, with ray-traced occlusion and reverb (
AcousticScene) - Dear ImGui integration
- Builds on Windows, Linux, macOS, MinGW and with Emscripten
Applications

threepp_editor: viewport, hierarchy, inspector, undo/redo and a PhysX-backed Play mode, with Python behaviour scripts. Scenes save as plain three.js JSON with the editor's data inuserData, so they also run in an ordinary threepp program. Prebuilt for Windows:pip install threepp-editor. See doc/editor.md.threepp_player: the same play runtime, headless, for CI and batch episodes. See doc/player.md.
Both build alongside the library in a top-level build (THREEPP_BUILD_EDITOR).
Project status
threepp is built for research, prototyping and teaching, not as a production engine. APIs change, so pin a tag or commit if you need reproducibility.
The three.js port is the mature layer, and the one most users touch. The simulation layer on top (the Vulkan renderer, sensors, PhysX, the editor and Python) is where development goes, and it moves fastest. Limits worth knowing:
- The Vulkan backend is built around the ray-tracing extensions (ray query and acceleration
structures). On a device without them it runs as a raster renderer: shadow maps, sky and
environment light only, environment-only reflections, no ray-traced AO and no lidar. That fallback
is exercised by forcing it on RT hardware and in CI, not yet on a real non-RT GPU or on macOS,
where MoltenVK has no ray tracing.
GLRendereris the conservative choice. - Gaussian splats cast no shadows and are invisible to the ray-traced sensors unless baked into a surface.
- The editor is tested on OpenGL; its Vulkan viewport is best-effort.
- CI has no GPU. The Vulkan backend runs there on lavapipe under the validation layers, which gates spec correctness, not pixels.
Example
#include "threepp/threepp.hpp"
using namespace threepp;
int main() {
Canvas canvas{"Demo"};
GLRenderer renderer{canvas};
auto scene = Scene::create();
auto camera = PerspectiveCamera::create(75, canvas.aspect(), 0.1f, 100.f);
camera->position.z = 5;
OrbitControls controls{*camera, canvas};
scene->add(HemisphereLight::create());
auto material = MeshPhongMaterial::create();
material->color = Color::green;
auto box = Mesh::create(BoxGeometry::create(), material);
scene->add(box);
canvas.onWindowResize([&](WindowSize size) {
camera->aspect = size.aspect();
camera->updateProjectionMatrix();
renderer.setSize(size);
});
Clock clock;
canvas.animate([&] {
box->rotation.y += 1.f * clock.getDelta();
renderer.render(*scene, *camera);
});
}
Swap GLRenderer for VulkanRenderer and the scene code stays the same. Math classes are value
types; everything else is created through a static ::create that returns a std::shared_ptr,
and GPU resources are released when the last reference goes. threepp/threepp.hpp covers the
three.js core; extras/, postprocessing/, splats/ and the newer objects are included explicitly.
Python
pip install threepp
Wheels for CPython 3.10 to 3.14 on Windows x64 and manylinux x86_64 carry the OpenGL and Vulkan renderers and CPU PhysX; the Windows wheels also carry the FSR 3.1 upscaler. macOS builds from source (OpenGL only). Releases are named by date; pin the one you tested.
import threepp as tp
canvas = tp.Canvas("offscreen", width=800, height=600, headless=True)
renderer = tp.GLRenderer(canvas)
scene = tp.Scene()
camera = tp.PerspectiveCamera(75, 800 / 600, 0.1, 100)
camera.position.z = 5
mat = tp.MeshStandardMaterial()
mat.color = 0x00aaff
scene.add(tp.Mesh(tp.BoxGeometry(), mat))
scene.add(tp.HemisphereLight())
renderer.render(scene, camera)
pixels = renderer.read_pixels() # (H, W, 3) uint8 NumPy array
renderer.save_frame("out.png")
The module also exposes the Vulkan labels as NumPy arrays, PhysX articulations, CUDA interop and
threepp.rl, a GPU-vectorized PPO stack. A headless canvas needs no display, so the same code runs
on Colab and cloud GPUs. python/README.md is the full guide, with the extras
(threepp[rl], threepp[editor]) and the Python examples.
Building
All core dependencies are bundled. CMakePresets.json holds the configurations CI builds:
| Preset | What you get |
|---|---|
gl | OpenGL backend, with examples, tests and the editor (start here) |
gl-debug | as gl, unoptimised and with debug info |
vulkan | the deferred Vulkan renderer (Vulkan SDK on PATH, or the vcpkg vulkan feature) |
vulkan-aaa | as vulkan, plus the FSR 3.1 and DLSS upscalers (Windows) |
python | the pybind11 threepp module |
no-glfw | build check only: no GLFW frontend, so no rendering |
wasm | Emscripten/WebGL2 examples (needs an activated emsdk) |
cmake --preset gl
cmake --build --preset gl
ctest --preset gl
Each preset builds into build/<preset>. cmake --list-presets also lists the ci-* presets the
GitHub workflow runs, and machine-specific presets belong in an untracked CMakeUserPresets.json
that inherits from these. Without presets, cmake -B build and cmake --build build work as
usual; add -DTHREEPP_USE_EXTERNAL_GLFW=ON to use a system GLFW.
A subset of the examples runs in the browser: try them online.
Some headers need extra packages when you consume them:
| Header | Dependency | Notes |
|---|---|---|
| AssimpLoader | assimp | Import a wide range of 3D formats |
| ImguiContext | imgui | Dear ImGui utility |
| Physx*, ConvexDecomposition | physx, v-hacd | Physics simulation and concave colliders |
| Vulkan*, Ocean, ParticleField, SonarSensor, and others | Vulkan SDK | Linked only under THREEPP_WITH_VULKAN, which the threepp target propagates |
Using threepp in your project
CMake FetchContent is the recommended and tested route:
include(FetchContent)
FetchContent_Declare(
threepp
GIT_REPOSITORY https://github.com/markaren/threepp.git
GIT_TAG tag_or_branch
GIT_SHALLOW TRUE
)
FetchContent_MakeAvailable(threepp)
target_link_libraries(main PUBLIC threepp::threepp)
Consumed this way only the library builds, and the example assets in threepp_data are never downloaded. A complete setup is in tests/threepp_fetchcontent_test, and threepp_wxwidgets embeds threepp in a wxWidgets window. threepp is also on Conan Center, which xmake can consume too; see doc/package_managers.md for both and their caveats.
Why?
Because C++ deserves nice things too. Also, because fun.
It also turns out to suit AI-assisted development: a three.js API that models already know, few dependencies, first-party source all the way down to the Vulkan shaders, and a short loop from code to a rendered image that can be captured headless and judged.
Gallery
License
threepp is MIT-licensed. It ports the API and shaders of
three.js (MIT) and bundles or fetches a number of third-party
libraries and assets; see THIRD_PARTY.md for the full index, including the parts
that are not MIT (the optional NVIDIA DLSS SDK, and the Boston Dynamics Spot model in threepp_data).