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.

2026-11-04发布于 2026-11-04 19:32 UTC1,226star +14 / 7 天158fork同步于 2026-11-10 11:49 UTC

怎么上手

由 README 提炼,执行前请确认
cmake --preset gl
cmake --build --preset gl
ctest --preset gl
硬件要求按 README 说明

README 未给出具体显存或 GPU 型号要求;Vulkan 后端依赖光追扩展(ray query 与加速结构),不支持时退化为光栅渲染,macOS 上 MoltenVK 无光追,保守选择是 GLRenderer。

需要 CUDA
下载:源码 zip·GitHub Releases(安装包)本站不镜像安装包

项目说明

由 README 自动翻译提炼

用 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 安装

容易踩的坑

Vulkan 预设需要 Vulkan SDK 在 PATH 中或启用 vcpkg 的 vulkan feature;Assimp、ImGui、PhysX、V-HACD 等头文件需额外依赖才能使用;用 FetchContent 引入时不会下载 threepp_data 示例资源。

内容由模型根据 README 生成,可能不完整或不准确,以官方文档为准。看原文 README

最近发版

5 个版本2026-11-04
  1. 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

    • FBXLoader is removed, with THREEPP_WITH_FBX, the OpenFBX fetch, the Python binding and the fbx_loader example. scripts/fbx_to_gltf.py converts 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 through MSFT_texture_dds, which GLTFLoader now reads.
    • RLtools is removed: the rltools_pendulum and rltools_swarm demos, the fetch, THREEPP_WITH_RLTOOLS and the third-party entry.
    • TypedBufferAttribute::copyAt, copyArray, copyColorsArray and copyVector{2,3,4}sArray are gone. Nothing called them.
    • Renderer::gammaFactor is gone, with the GAMMA_FACTOR define no shader read, and so is the lightmap_fragment shader chunk. ImguiContext::makeDpiAware() (a deprecated stub) is gone, and ImguiContext.hpp no longer includes Monitor.hpp: include it yourself where you use the monitor namespace. UrbanProps::lotBayPitch, lotRowPitch and lotOccupancy are gone (read nowhere).
    • Articulation::addLink throws 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 in joint_positions or set_drive_targets. addLink also takes a PhysX drive type as a trailing argument.
    • Python GLRenderer.read_pixels(deferred=True) returns None on the first call (the frame queued the call before); flush_pixels() drains the ring. The synchronous call is unchanged.
    • Python VulkanRenderer.render_scale raises ValueError outside [0.25, 1.0] instead of clamping.
    • Python Flock.set_perch_filter takes any callable (the predicate is wrapped; None clears 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, .parent and the lights' get_target no longer tie the result to self for 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 with 0, an integer or number falls back to the
  2. 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, MarbleRun and SandTable.

    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() returns const std::string& on Object3D, Material, BufferGeometry and everything under them. It returned a std::string by 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 overrides type() must take the new return type.
    • Font::glyphs is keyed by Unicode code point, not by char.
    • Removed from VulkanRenderer: setDeferredDenoise / deferredDenoise (use setDenoise / denoise), setVolumetricFog (a no-op since the fog unification; Python's volumetric_fog goes with it), and the VulkanRendererCore.hpp header and alias.
    • FireEffect::Params::legacyEmbers and emberRate are gone, with vulkan_fire --legacy-embers. The embers are a ParticleField.
    • PhysxVehicle: Settings::stickyDampingLongitudinal and stickyDampingLateral are gone. The vehicle now chooses between stickyDamping and stickyDampingHold each step (see the grade hold below).
    • Texture::premultiplyAlpha is removed. Neither renderer read it. The key in an old scene file is ignored on load.
    • Material::clipping is gone from the base class. It shadowed MaterialWithClipping::clipping, which is the one that remains.
    • PhysxSoftBody::registerGlTexture returns bool, like registerVulkanMemory.
    • THREEPP_DLSS_JITTER_SIGN is removed. The per-axis THREEPP_DLSS_JITTER_SIGN_X / _Y stay.
    • ABI: many classes changed layout. Rebuild everything that links threepp.

    Behaviour

    • Canvas::Parameters::fastExit is 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 in vkDestroyDevice. An app that tears its renderer down and builds a new one while the process goes on sets fastExit(false). Python's Canvas(fast_exit=...) keeps its False default, since interpreter exit is covered automatically.
    • VulkanRenderer is 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 to GLRenderer asks rayTracingSupported() (Python ray_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's save_frame used 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: VulkanRenderer draws an OrthographicCamera as 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
  3. 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 with BUILD_SHARED_LIBS=ON failed to link libthreepp.dll with relocation truncated to fit: IMAGE_REL_AMD64_SECREL. The same happened when threepp was pulled in with FetchContent. ObjectLoader.cpp compiled to 33,914 COFF sections, which is over the 32,767 a regular object file can hold.

      • ObjectLoader.cpp is now split in two. ObjectLoaderResources.cpp builds the images, textures, materials, geometries and animations, and ObjectLoader.cpp builds the object tree from them. Each half is about 20k sections.
      • GLTFLoader.cpp was next in line at 25,498 sections, so it is now split into three files: GLTFLoader.cpp, gltf/GLTFMaterials.cpp and gltf/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_core is only built when something needs it. Projects that consume threepp through FetchContent or add_subdirectory no 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-mingw job. 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-obj keeps the build green.
  4. 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::texCoord is replaced by Texture::channel (the three.js r152 name): 0 = uv, 1 = uv2, 2 = uv3. Nothing ever read texCoord; the glTF loader now routes texCoord and KHR_texture_transform's override into channel. An aoMap or lightMap left at channel 0 still samples uv2 when the geometry has one.
    • ShaderChunk::lights_lambert_vertex() is gone. Lambert is lit per fragment now (lights_lambert_pars_fragment / lights_lambert_fragment). Custom ShaderMaterials that included the vertex chunk need the fragment pair.
    • The Vulkan Ids G-buffer AOV is 4× uint32, 16 bytes per pixel (was 4× uint16, 8 bytes). This applies to readGBufferAOV / readViewGBufferAOVs and the Ids frame-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 match LidarReturn::hitInstanceId. Python decoders read uint32, and cuda_interop exports ids as '<u4', falling back to an int32 view where torch rejects uint32.
    • VulkanRenderer refuses a device without VK_KHR_ray_query and 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; use EglContext + GLRenderer there (see below).
    • Renderer::checkShaderErrors defaults to true, 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 the Renderer base class. VulkanRenderer::EnvSunPolicy is an alias, so existing spellings compile.
    • ABI: Texture, Material, BufferGeometry, RenderTarget and InstancedMesh lose a private member, Scene gains five fields, and LightShadow::copy is 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. EffectComposer appends an implicit OutputPass (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, Water and 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 with RenderTarget::displayTarget; CameraSensor does, 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 visible DirectionalLight. 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
  5. 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 GeoScene terrain stack.

    Breaking changes

    • Object3D::lookAt() now applies the camera convention (local -Z toward the target) to sensors. New virtual Object3D::usesCameraLookAtConvention() (default false) is overridden by TracedRasterVisionSensor (so DepthSensor and LidarSensor), PathTracedLidarSensor, CameraSensor and SonarSensor. Code that compensated with the mirror point lookAt(2*position - target) now aims backwards and must pass the target itself. Adding a virtual to Object3D changes its vtable layout.
    • Imu defaults are the ICM-42688-P datasheet figures: gyroNoise 0.005 → 4.8869e-5 rad/s/√Hz, accelNoise 0.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() and OS0_128() step azimuth at 360/1024° (0.3515625) instead of 0.35°: 1024 columns per revolution instead of 1029.
    • BVH::intersects() and BVH::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, and getGeometry() returns nullptr after the array build.
    • ArticulationLink's constructor gained a leading const PhysxWorld* parameter.
    • PhysxGpuBatch::blockFloatsWrite throws std::runtime_error for unhandled write types (including the four tendon types) instead of returning maxDofs.
    • ParticleField::submit gained float dtSec = 0.f, and VulkanRenderer::enableVertexInterop gained bool stableCorrespondence = true. Both source-compatible; both ABI-breaking.
    • RoadNetwork::buildBridgeMeshes() and buildGroundChunkMeshes() keep their signatures but ignore texWidth/texHeight. Resolution, tile length, UVs, vertex/index counts and materials now come from MarkingRules; 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 three geo_buildings_walls, _walls_plain, _roof. Lookups by name now fail at runtime rather than at compile time.
    • terrain::detail::geoBldRoofPalette() was removed from GeoBuildings.hpp. Buildings with no OSM colour tag render in different colours.
    • makeNorwegianSplat() emits eight layers instead of six (new shallow and seabed layers; wetland heightLo moved 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::landCover defaults to true and pack.flow is always computed, so wet-rock and scree tinting apply to every existing pack.
    • loadSogWithLod throws 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=2048 restores the old density); the GL fogColor uniform is encoded at upload.

    Added

    Sensors

    • SonarSensor (new header threepp/helpers/SonarSensor.hpp) — Vulkan-only forward-looking imaging sonar, traced through the renderer's acceleration structure as one dispatch. Synchronous scan() plus the pipelined scanBegin/scanFired/scanReady/scanCollect.
    • SonarModel (new head

用户点评

登录用户的真实使用记录,可按版本和硬件筛选
共 0 条

    还没有人点评。第一个说出真实体验的人最有价值。

    README 原文

    未翻译

    threepp

    Build License: MIT Conan Center PyPI

    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 in a digital twin of a waterfront in Ålesund 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
    • EffectComposer with 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 .tpz archive; 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 view
    • ParticleField (Vulkan): GPU particles for fire, smoke, snow, rain and physics-coupled grains, drawn as meshes or billboards, or marched as participating media
    • Gaussian splatting: SplatCloud renders .ply and 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; addView renders 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 setSimTime from 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

    The threepp scene editor

    • 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 in userData, 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. GLRenderer is 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:

    PresetWhat you get
    glOpenGL backend, with examples, tests and the editor (start here)
    gl-debugas gl, unoptimised and with debug info
    vulkanthe deferred Vulkan renderer (Vulkan SDK on PATH, or the vcpkg vulkan feature)
    vulkan-aaaas vulkan, plus the FSR 3.1 and DLSS upscalers (Windows)
    pythonthe pybind11 threepp module
    no-glfwbuild check only: no GLFW frontend, so no rendering
    wasmEmscripten/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:

    HeaderDependencyNotes
    AssimpLoaderassimpImport a wide range of 3D formats
    ImguiContextimguiDear ImGui utility
    Physx*, ConvexDecompositionphysx, v-hacdPhysics simulation and concave colliders
    Vulkan*, Ocean, ParticleField, SonarSensor, and othersVulkan SDKLinked 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).