Skip to content

About

Three.js core and WebGPURenderer ported to Rust on wgpu, graded by Three's own e2e pixel diff

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

 
 

Repository files navigation

three-rs

CI

A port of three.js core and its WebGPURenderer to Rust on wgpu, graded by Three's own end-to-end pixel comparison: each ported example renders headless and is diffed against the reference screenshot Three ships for that example, using Three's unmodified test/e2e/image.js comparator.

Two things distinguish it from other three.js-shaped Rust crates (including the similarly named threers): the shaders are not hand-written but generated from Three's node graph (TSL) by a port of Three's NodeBuilder, the way WebGPURenderer does it, so Three's own WGSL dumps are the reference; and correctness is judged by Three's own examples and reference screenshots, not by scenes written for the port.

Status: 100 of 215 gradeable three.js WebGPU pages graded (16 N.A.). Early, working, incomplete. Three ships 231 webgpu_* pages; 16 cannot grade against a reference frame (XR sessions, editor UIs, benchmarks, video and DOM layout: the page table in docs/parity.md says which and why), a few more are ported but ignored because Three fails its own reference on the grading machine (the same table lists them), and the webgpu_* examples in the gallery below pass the grader. The rest (and Three's 600-odd examples overall) have not been attempted. The API follows Three's object model but is not stable. Vulkan on Linux is the only backend that has been run.

docs/parity.md judges every three.js export, row by row, as present, partial or absent in the port, with the test or graded example that verifies each present row.

What is ported

One paragraph per area, describing what the code holds today and the larger gaps. docs/parity.md is the row-level truth: every three.js export, its verdict and the test or graded example that checks it.

Math and core. The math library whole: vectors, Matrix2/3/4, Quaternion, Euler, Color with ColorManagement, Box2/Box3, Sphere, Plane, Ray, Line3, Triangle, Frustum, Spherical, Cylindrical, SphericalHarmonics3 and the interpolants, each with its QUnit port. MathUtils has the interpolation and power-of-two helpers but not the random, UUID or normalize ones, and there is no BezierInterpolant. The Object3D scene graph with layers, typed scene events and render hooks; BufferGeometry with groups, draw ranges, morph and instanced attributes; typed BufferAttributes (every array type three uses, normalized) and interleaved buffers; Raycaster and Timer. All the cameras (perspective, orthographic, array, cube, stereo), Scene, Fog and FogExp2. The objects: Mesh, Sprite, Points, Line, LineSegments, InstancedMesh, BatchedMesh, SkinnedMesh with Skeleton and Bone, Group and ClippingGroup; not LOD. The core helpers (axes, arrow, grid, polar grid, box, Box3, plane, camera, skeleton, and the directional, hemisphere, point and spot light helpers).

Geometries. Every core geometry except EdgesGeometry and WireframeGeometry: box, plane, sphere, circle, ring, cylinder, cone, torus, torus knot, capsule, lathe, the polyhedra, shape, extrude and tube, plus the TeapotGeometry and RoundedBoxGeometry addons, bit-exact against samples generated from three. The curve family (Curve, CurvePath, lines, Béziers, ellipses and arcs, splines, CatmullRomCurve3), Path / Shape / ShapePath and ShapeUtils with Earcut, with their QUnit ports and a 1e-6 oracle against three's own output.

Node system (TSL). A NodeBuilder that generates both WGSL stages and the bind group layouts from a node graph, following three's nodes/ and renderers/webgpu/nodes/, and most of the three/tsl exports, with the generated WGSL gated against three's own dumps; deliberate divergences are listed in docs/nodes.md. It covers the material and lighting flow, morphing, skinning and batching, MRT, velocity, clipping, PMREM, the reflector, screen reads, compute (storage buffers and textures, atomics, barriers, workgroup memory, subgroups, indirect dispatch), the MaterialX noise library, wgslFn / code and user-defined nodes through Node::Custom. The post-processing nodes of examples/jsm/tsl/display (bloom, GTAO, SSAO, SSR, SSGI, SSS, TRAA, TAAU, FSR1, SMAA, FXAA, depth of field, outline, OIT, the stereo passes, the denoisers and the rest) live in nodes::display. Missing: reference() / materialReference() by name, custom tone mapping, and JSON node loading.

Materials. Basic, Lambert, Phong, Standard, Physical (clearcoat, sheen, anisotropy, transmission and volume, iridescence, specular and ior, diffuse roughness), Toon, Normal, Points, Sprite, LineBasic and the fat Line2NodeMaterial (no dashes), as node materials with their node slots. Bump and normal maps, environment maps, blending modes and BlendMode, alpha test and alpha hash, wireframe, stencil state, backdrop nodes, fog and tone mapping. Absent: ShadowMaterial (the shadow pass uses an internal override material, not it), depth, distance, matcap, dashed-line and SSS materials, dispersion, and the light, displacement and specular maps. A field the port does not read is reported, not silently dropped (docs/api.md §7).

Lights and shadows. Ambient, hemisphere, point, spot (no projected map), directional and LightProbe (nine spherical-harmonic coefficients, irradiance only), and a user LightingModel. Planar and cube shadow maps with three's filters: basic, PCF on the Vogel disk (PCFSoft resolves to it, as in three's WebGPU renderer) and VSM, plus shadow opacity, skinned casters and custom shadowNode / filterNode. RectAreaLight, IESSpotLight and ProjectorLight are absent.

Renderer. One Renderer on wgpu: Vulkan natively, the browser's WebGPU in the web shell. Render lists, instancing, morph targets, skinning and batching, render targets (MSAA, MRT, mip levels, red formats), RenderPipeline, DirectRenderPipeline, PassNode post-processing and QuadMesh, PmremGenerator, CubeCamera / CubeRenderTarget, mipmaps, line topology, viewport / scissor / clearDepth / autoClear, occlusion queries, copyTextureToTexture and pixel and storage-buffer readback. Compute with indirect dispatch and indirect draws, storage textures (2D and 3D) and storage buffers. The texture family: Texture, DataTexture, Data3DTexture, CubeTexture, DepthTexture and compressed 2D and array textures uploaded from KTX2 (compressed cube and 3D textures parse but do not upload). Velocity as an MRT output, with three's previous-frame matrices, under temporal anti-aliasing and motion blur; weighted blended order-independent transparency on per-attachment blend modes; a PassNode that draws at a fraction of the canvas and swaps each draw's material (retroPass); clipping planes through ClippingGroup (union and intersection, hardware clip_distances where the device has them, alpha-to-coverage edges) and per-material stencil state. Not ported: BundleGroup, compileAsync, copyFramebufferToTexture, storage array textures, and cache eviction.

Addons. src/addons/ holds the three/addons/… tier that the graded examples import: controls (OrbitControls, FirstPersonControls, FlyControls, TransformControls, each graded against the JS class itself), lines (fat lines: LineSegmentsGeometry, LineGeometry, LineSegments2, Line2), text_geometry, curve_modifier_gpu (Flow), lights (LightProbeGenerator), helpers (LightProbeHelper), objects (SkyMesh, WaterMesh, Water2Mesh), camera_utils, improved_noise, simplex_noise, raymarching (RaymarchingBox), textures (FlakesTexture) and geometry_utils (hilbert3D only). RoomEnvironment is in environments, SortUtils' radix sort in utils, and the tsl/display effect nodes in nodes::display (above). They live in the root crate for the harness reason given under Addons.

Loaders. glTF/GLB: every accessor type, skins, animations, KHR_mesh_quantization, KHR_draco_mesh_compression, EXT_meshopt_compression, KHR_texture_basisu, KHR_texture_transform, EXT_texture_webp, and the KHR_materials_* extensions (specular, ior, clearcoat, sheen, transmission, volume, anisotropy, iridescence, diffuse roughness, emissive strength, unlit); not cameras, KHR_lights_punctual, cubic-spline animation (linearised) or AVIF. Textures from PNG, JPEG, GIF and WebP (an AVIF image is refused with an error), cube textures, KTX2, HdrLoader, HdrCubeTextureLoader, UltraHdrLoader, BufferGeometryLoader, ObjLoader (faces, no MTL), the LUT loaders (.cube, .3dl, image) and typeface.json fonts (FontLoader). Every loader is synchronous; there is no LoadingManager, Cache or ObjectLoader.

Animation. Keyframe tracks of every value type, clips, PropertyMixer, PropertyBinding (position, quaternion, scale and morph influences), AnimationAction, AnimationMixer (its loop / finished events are a MixerEvent queue drained with take_events()), AnimationObjectGroup and AnimationUtils. Bézier tracks have no interpolant.

Workspace crate. sdf-text: signed-distance-field text rendering with a BatchedText object (ttf-parser outlines, analytic rasteriser), and the SDF text examples with their gates. Its d3_treemap_labels example lays its treemap out with d3-hierarchy, a port of d3-hierarchy 3.1.2 that began in this repository.

Examples graded green

webgpu_depth_texture webgpu_instance_mesh webgpu_materials_basic webgpu_rtt
webgpu_depth_texture webgpu_instance_mesh webgpu_materials_basic webgpu_rtt
webgpu_lights_phong webgpu_morphtargets webgpu_shadowmap webgpu_lights_physical
webgpu_lights_phong · notes webgpu_morphtargets · notes webgpu_shadowmap · notes webgpu_lights_physical · notes
webgpu_postprocessing_masking webgpu_tsl_galaxy webgpu_skinning webgpu_mesh_batch
webgpu_postprocessing_masking · notes webgpu_tsl_galaxy · notes webgpu_skinning · notes webgpu_mesh_batch · notes
webgpu_postprocessing_radial_blur webgpu_materials webgpu_postprocessing_ssaa webgpu_pmrem_cubemap
webgpu_postprocessing_radial_blur · notes webgpu_materials · notes webgpu_postprocessing_ssaa · notes webgpu_pmrem_cubemap · notes
webgpu_postprocessing_bloom_selective webgpu_compute_points webgpu_lines_fat webgpu_lines_fat_raycasting
webgpu_postprocessing_bloom_selective · notes webgpu_compute_points · notes webgpu_lines_fat · notes webgpu_lines_fat_raycasting · notes
webgpu_pmrem_test webgpu_postprocessing_difference webgpu_postprocessing_direct webgpu_furnace_test
webgpu_pmrem_test · notes webgpu_postprocessing_difference · notes webgpu_postprocessing_direct · notes webgpu_furnace_test · notes
webgpu_postprocessing_anamorphic webgpu_pmrem_scene webgpu_postprocessing_bloom webgpu_materials_envmaps
webgpu_postprocessing_anamorphic · notes webgpu_pmrem_scene · notes webgpu_postprocessing_bloom · notes webgpu_materials_envmaps · notes
webgpu_materials_cubemap_mipmaps webgpu_postprocessing_bloom_emissive webgpu_instance_uniform webgpu_tsl_interoperability
webgpu_materials_cubemap_mipmaps · notes webgpu_postprocessing_bloom_emissive · notes webgpu_instance_uniform · notes webgpu_tsl_interoperability · notes
webgpu_pmrem_equirectangular webgpu_postprocessing_ca webgpu_loader_gltf webgpu_mrt
webgpu_pmrem_equirectangular · notes webgpu_postprocessing_ca · notes webgpu_loader_gltf · notes webgpu_mrt · notes
webgpu_custom_fog_background webgpu_loader_gltf_sheen webgpu_deferred webgpu_loader_gltf_anisotropy
webgpu_custom_fog_background · notes webgpu_loader_gltf_sheen · notes webgpu_deferred · notes webgpu_loader_gltf_anisotropy · notes
webgpu_loader_gltf_transmission webgpu_materials_texture_manualmipmap webgpu_tsl_vfx_flames webgpu_procedural_texture
webgpu_loader_gltf_transmission · notes webgpu_materials_texture_manualmipmap · notes webgpu_tsl_vfx_flames · notes webgpu_procedural_texture · notes
webgpu_postprocessing_sobel webgpu_postprocessing_transition webgpu_tsl_raging_sea webgpu_tsl_angular_slicing
webgpu_postprocessing_sobel · notes webgpu_postprocessing_transition · notes webgpu_tsl_raging_sea · notes webgpu_tsl_angular_slicing · notes
webgpu_textures_2d-array_compressed webgpu_compute_texture webgpu_volume_perlin webgpu_shadowmap_vsm
webgpu_textures_2d-array_compressed · notes webgpu_compute_texture · notes webgpu_volume_perlin · notes webgpu_shadowmap_vsm · notes
webgpu_shadowmap_pointlight webgpu_struct_drawindirect webgpu_particles webgpu_clearcoat
webgpu_shadowmap_pointlight · notes webgpu_struct_drawindirect · notes webgpu_particles · notes webgpu_clearcoat · notes
webgpu_modifier_curve webgpu_camera_array webgpu_sprites webgpu_instance_sprites
webgpu_modifier_curve · notes webgpu_camera_array · notes webgpu_sprites · notes webgpu_instance_sprites · notes
webgpu_materials_toon webgpu_occlusion webgpu_fog_height webgpu_shadowmap_opacity
webgpu_materials_toon · notes webgpu_occlusion · notes webgpu_fog_height · notes webgpu_shadowmap_opacity · notes
webgpu_multiple_rendertargets webgpu_multiple_rendertargets_readback webgpu_texturegrad webgpu_texturegather
webgpu_multiple_rendertargets · notes webgpu_multiple_rendertargets_readback · notes webgpu_texturegrad · notes webgpu_texturegather · notes
webgpu_postprocessing_fxaa webgpu_lights_selective webgpu_loader_gltf_compressed webgpu_equirectangular
webgpu_postprocessing_fxaa · notes webgpu_lights_selective · notes webgpu_loader_gltf_compressed · notes webgpu_equirectangular · notes
webgpu_skinning_points webgpu_instance_points webgpu_textures_partialupdate webgpu_layers
webgpu_skinning_points · notes webgpu_instance_points · notes webgpu_textures_partialupdate · notes webgpu_layers · notes
webgpu_loader_gltf_diffuse_roughness webgpu_cubemap_mix webgpu_mirror webgpu_tsl_halftone
webgpu_loader_gltf_diffuse_roughness · notes webgpu_cubemap_mix · notes webgpu_mirror · notes webgpu_tsl_halftone · notes
webgpu_tsl_earth webgpu_lightprobe webgpu_lightprobe_cubecamera webgpu_sky
webgpu_tsl_earth · notes webgpu_lightprobe · notes webgpu_lightprobe_cubecamera · notes webgpu_sky · notes
webgpu_backdrop webgpu_postprocessing_motion_blur webgpu_display_stereo webgpu_ocean
webgpu_backdrop · notes webgpu_postprocessing_motion_blur · notes webgpu_display_stereo · notes webgpu_ocean · notes
webgpu_postprocessing_godrays webgpu_postprocessing_lensflare webgpu_postprocessing_dof_basic webgpu_postprocessing_ssr
webgpu_postprocessing_godrays · notes webgpu_postprocessing_lensflare · notes webgpu_postprocessing_dof_basic · notes webgpu_postprocessing_ssr · notes
webgpu_postprocessing_3dlut webgpu_postprocessing_outline webgpu_oit webgpu_clipping
webgpu_postprocessing_3dlut · notes webgpu_postprocessing_outline · notes webgpu_oit · notes webgpu_clipping
webgpu_clipping_stencil webgpu_loader_gltf_iridescence webgpu_materials_transmission webgpu_portal
webgpu_clipping_stencil webgpu_loader_gltf_iridescence · notes webgpu_materials_transmission · notes webgpu_portal · notes

Our own rendered frames, one per graded example. Each thumbnail opens the example running in your browser on WebGPU (all of them); the caption links the ported source. See docs/gallery.md.

Every one of these also runs in a browser, on the browser's own WebGPU, from the same init(): the graded frame first, then the page's own animation loop, with drag to orbit, wheel to dolly and right-drag to pan. See web/README.md for how to build and serve the shell, and issue #128 for where that is going. The table's browser column is CI's web-gate job: the same example in headless Chrome on software WebGPU, graded against the same screenshots at the same threshold (see Grading in the browser).

Three example different pixels (of 100000) steady frame (ms) draw calls triangles browser
webgpu_depth_texture 0 11.0 43 671746 yes
webgpu_instance_mesh 60 (Three itself scores 60 against the same JPEG) 9.3 2 967001 yes
webgpu_materials_basic 0 16.1 118 113345 yes
webgpu_rtt 1 2.3 3 14 yes
webgpu_lights_phong 31 4.3 5 62001 yes
webgpu_morphtargets 0 2.8 2 12289 yes
webgpu_shadowmap 7 7.7 19 39399 yes
webgpu_lights_physical 4 4.2 11 4267 yes
webgpu_postprocessing_masking 18 1.0 3 1037 yes
webgpu_tsl_galaxy 40 5.3 2 40001 yes
webgpu_skinning 6 3.0 3 30091 yes
webgpu_mesh_batch 0 2.5 454 44341 yes
webgpu_postprocessing_radial_blur 7 3.9 2 401 yes
webgpu_materials 44 7.3 19 350065 yes
webgpu_postprocessing_ssaa 0 11.9 17 2119689 yes
webgpu_pmrem_cubemap 31 8.2 32 243905 yes
webgpu_postprocessing_bloom_selective 1 16.5 63 256013 yes
webgpu_compute_points 4 (see below) 10.7 2 1 + 300000 points yes
webgpu_lines_fat 0 3.8 6 11191 yes
webgpu_lines_fat_raycasting 57 1.7 2 901 yes
webgpu_pmrem_test 0 6.1 35 67457 yes
webgpu_postprocessing_difference 13 1.5 2 13 yes
webgpu_postprocessing_direct 21 4.9 93 4192 yes
webgpu_furnace_test 0 7.1 122 116161 yes
webgpu_postprocessing_anamorphic 2 8.1 16 398798 yes
webgpu_pmrem_scene 0 3.8 9 58433 yes
webgpu_postprocessing_bloom 0 8.6 19 52085 yes
webgpu_materials_envmaps 0 2.1 3 7105 yes
webgpu_materials_cubemap_mipmaps 1 2.6 3 65025 yes
webgpu_postprocessing_bloom_emissive 0 5.1 15 17449 yes
webgpu_instance_uniform 13 6.0 14 247105 yes
webgpu_tsl_interoperability 0 2.2 2 4 yes
webgpu_pmrem_equirectangular 0 5.9 32 243905 yes
webgpu_postprocessing_ca 0 3.4 23 5658 (+ 42 lines) yes
webgpu_loader_gltf 22 1.9 3 17437 yes
webgpu_mrt 10 2.8 3 17437 yes
webgpu_custom_fog_background 7 2.0 2 15453 yes
webgpu_loader_gltf_sheen 28 3.1 6 41921 yes
webgpu_deferred 0 2.8 25 26378 yes
webgpu_loader_gltf_anisotropy 94 3.2 5 12188 yes
webgpu_loader_gltf_transmission 6 2.9 6 26433 yes
webgpu_materials_texture_manualmipmap 81 2.5 11 19 yes
webgpu_tsl_vfx_flames 31 5.4 5 9 yes
webgpu_procedural_texture 0 3.2 5 6 yes
webgpu_postprocessing_sobel 0 2.8 3 91218 yes
webgpu_postprocessing_transition 0 1.4 2 40001 yes
webgpu_tsl_raging_sea 12 10.5 2 131073 yes
webgpu_tsl_angular_slicing 4 3.3 9 45397 yes
webgpu_textures_2d-array_compressed 0 6.1 1 2 yes
webgpu_compute_texture 0 1.2 2 3 yes
webgpu_volume_perlin 23 5.2 2 13 yes
webgpu_shadowmap_vsm 0 4.5 22 10419 yes
webgpu_shadowmap_pointlight 63 (Three itself scores 63 against the same JPEG) 5.0 20 7421 yes
webgpu_struct_drawindirect 0 (see below) 1.3 2 100001 (drawn indirect) yes
webgpu_particles 0 (see below) 5.8 4 6001 (+ 82 lines; the fire's 2000 drawn indirect) yes
webgpu_clearcoat 4 2.7 6 17857 yes
webgpu_modifier_curve 3 (Three itself scores 3 against the same JPEG) 1.3 7 34753 (+ 50 lines) yes
webgpu_camera_array 20 4.0 73 4808 yes
webgpu_sprites 0 13.8 201 401 yes
webgpu_instance_sprites 32 1.0 2 20001 yes
webgpu_materials_toon 0 58.9 438 421985 yes
webgpu_occlusion 0 1.9 3 963 yes
webgpu_fog_height 0 2.8 3 3185 yes
webgpu_shadowmap_opacity 9 6.0 6 408644 yes
webgpu_multiple_rendertargets 22 2.2 2 8193 yes
webgpu_multiple_rendertargets_readback 22 2.4 3 8194 yes
webgpu_texturegrad 0 2.2 4 6 yes
webgpu_texturegather 16 1.8 4 6 yes
webgpu_postprocessing_fxaa 56 (Three itself scores 56 against the same JPEG) 4.0 3 402 yes
webgpu_lights_selective 9 (Three itself scores 9 against the same JPEG) 4.6 5 62001 yes
webgpu_loader_gltf_compressed 4 6.1 3 332259 yes
webgpu_equirectangular 0 3.6 2 1985 yes
webgpu_skinning_points 44 7.5 2 32681 (16340 point quads) yes
webgpu_instance_points 16 21.5 6 3011 (256 point quads, drawn twice) yes
webgpu_textures_partialupdate 0 9.3 2 3 yes
webgpu_layers 76 (Three itself scores 76 against the same JPEG) 3.5 5 16985 yes
webgpu_loader_gltf_diffuse_roughness 0 8.6 26 75713 yes
webgpu_cubemap_mix 1 6.4 3 17437 yes
webgpu_mirror 22 6.4 40 6269 yes
webgpu_tsl_halftone 93 (Three itself scores 93 against the same JPEG) 3.1 4 44363 yes
webgpu_tsl_earth 0 2.6 3 16129 yes
webgpu_lightprobe 2 1.3 4 6913 yes
webgpu_lightprobe_cubecamera 0 1.6 3 2945 yes
webgpu_sky 0 5.8 3 3981 yes
webgpu_backdrop 23 (Three itself scores 23 against the same JPEG) 6.1 11 37771 yes
webgpu_postprocessing_motion_blur 5 6.2 9 100553 yes
webgpu_display_stereo 0 4.6 5 963969 yes
webgpu_ocean 0 4.5 19 75 yes
webgpu_postprocessing_godrays 3 12.1 54 9234 yes
webgpu_postprocessing_lensflare 0 3.3 29 55534 yes
webgpu_postprocessing_dof_basic 5 7.1 42 33694 yes
webgpu_postprocessing_ssr 4 10.2 17 22888 yes
webgpu_postprocessing_3dlut 0 1.9 4 5705 yes
webgpu_postprocessing_outline 15 (Three itself scores 15 against the same JPEG) 4.1 46 96243 yes
webgpu_oit 0 2.2 12 12045 yes
webgpu_clipping 17 4.5 5 11403 yes
webgpu_clipping_stencil 0 6.0 9 132007 yes
webgpu_loader_gltf_iridescence 0 5.2 5 20851 yes
webgpu_materials_transmission 16 6.3 4 9921 yes
webgpu_portal 77 24.9 9 53085 (and 147336 wireframe lines) yes

webgpu_compute_points is graded like the rest and its 4 pixels mean less than the rest: its frame is black apart from a 2x2 block at the centre, so Three's comparator would pass it at 0.0% even if the compute passes never ran. That rung is gated on the WGSL its kernels compile to and on reading the storage buffers back — docs/rung12-progress.md says why and what the tests assert.

webgpu_struct_drawindirect and webgpu_particles are the same kind of green. three's page draws before it dispatches the kernels that fill the indirect buffer, so the graded frame of the first is the clear colour; at the grader's pinned time every sprite of the second has zero opacity, so its frame is the grid. Both are gated on their shaders against three's own dump (tests/nodes_compute_indirect_wgsl.rs) and on what the GPU draws a frame or a moment later (tests/renderer_compute_indirect.rs); the notes say what each asserts. The triangle counts are the CPU-side counts renderer.info() records for an indirect draw, as three's does, not what the GPU was told to draw.

Some pages are ported but cannot be graded. Either three's own e2e harness skips them, or three.js itself misses its own reference screenshot on this machine, so a matching port would miss it too. Twelve pages that three misses here are only #[ignore]d rungs in tests/e2e/main.rs (docs/parity.md counts them, with retro, taau and fsr1, as fifteen). The pages below are in the native viewer, not in the gallery or the browser shell:

example why it has no grade
webgpu_postprocessing_traa · notes in three's e2e exception list (test/e2e/puppeteer.js, "Black screen"). It is gated instead on its resolve shader against three's dump and on tests/traa_frames.rs
webgpu_water · notes in three's e2e exception list (test/e2e/puppeteer.js, "1 min"). It is gated instead on the water's shaders against three's dumps (tests/nodes_water_wgsl.rs) and on tests/water2_frames.rs
webgpu_postprocessing_sss · notes in the same exception list. It is gated instead on its SSS shader against three's dump and on tests/sss_frames.rs
webgpu_postprocessing_ssgi · notes in three's e2e exception list (test/e2e/puppeteer.js, "Black screen"). It is gated instead on its SSGI, composite and TRAA shaders against three's dump and on tests/ssgi_frames.rs
webgpu_postprocessing_ao · notes in the same exception list. It is gated instead on its GTAO shader against three's dump and on tests/gtao_frames.rs
webgpu_postprocessing_dof · notes in three's e2e exception list (test/e2e/puppeteer.js, "Black screen"). It is gated instead on each of DepthOfFieldNode's seven distinct quad shaders against three's dump (dof_* in tests/nodes_display_wgsl.rs)
webgpu_postprocessing_retro · notes three.js scores 1.5% against its own reference here, over the 0.1% limit, and the port scores the same 1503 pixels. It is gated instead on its two post-processing shaders against three's dump and on tests/retro_frames.rs
webgpu_postprocessing_ssr_denoise · notes in three's e2e exception list (test/e2e/puppeteer.js, "Need more time to render"). It is gated instead on its scene material, SSR, denoise, grading and sharpen shaders against three's dump and on tests/ssr_denoise_frames.rs
webgpu_upscaling_taau · notes three.js scores 0.5% against its own reference here (540 pixels), over the 0.1% limit, and the port scores 539. It is gated instead on its seed and resolve shaders against three's dump and on tests/taau_frames.rs
webgpu_upscaling_fsr1 · notes three.js scores 0.7% against its own reference here, over the 0.1% limit, and the port scores 698 pixels to three's 703. It is gated instead on its EASU and RCAS shaders against three's dump and on tests/fsr1_frames.rs

Measured on Intel Iris Xe, Mesa 25.3.6, Fedora 43, against three.js 5f610f5 (past r186, for the cube PMREM of 2f80402; the pin becomes the r187 tag once upstream tags it). Other GPUs and drivers will land somewhere else on the pass threshold; the threshold is Three's own (0.1% of pixels).

The steady frame is the whole cost of a frame once the first has built and uploaded everything — the example's animate() through to the GPU finishing it, at 800x500, mean of 30 frames after a 10-frame warm-up in a release build (viewer <example> --headless --frames 40, below). The same machine caveat applies. The e2e grader renders three more frames after the graded one and fails a rung whose steady frame is over a ceiling set well above these (STEADY_FRAME_CEILING in tests/e2e/main.rs), so a per-frame cost the single graded frame cannot see fails the ladder.

The last two columns are renderer.info() for the whole graded frame — draw calls and the triangles they drew, instancing multiplied in (webgpu_rtt's 14 is a cube and two full-screen quads; webgpu_instance_mesh's million is one instanced draw). Unlike the time, these are exact: the same grader asserts that frames two and three of every rung compile, build and upload nothing, as an equality, which is what catches a regression that re-uploads a live geometry every frame while staying well under a time ceiling. Info has the rest of the counts (pipelines, geometries, attribute buffers, textures, and the resident totals).

Building

(For how to contribute, what is in scope, and the gates a change has to pass, see CONTRIBUTING.md.)

Requires a Rust toolchain (1.90 or newer) and a Vulkan driver. wgpu comes from crates.io, pinned to 30.0.1 in Cargo.toml; nothing else is unusual.

wgpu versions

wgpu types are part of the public API (a caller-owned Device and Queue, external textures), so an application that uses wgpu directly must use the same wgpu major version as three-rs, or it builds two copies that cannot exchange values. Each three-rs minor release tracks one wgpu major: 0.1, 0.2 and 0.3 are on wgpu 30. Moving to a new wgpu major is a breaking change, so it comes in the next minor release (0.3 to 0.4), never in a patch release.

cargo build --release
cargo test -p sdf-text --lib                # sdf-text unit tests; no GPU
cargo test -p three-rs --lib                # three-rs unit tests; no GPU

The rest of cargo test --workspace needs a GPU (the renderer tests and the SDF text gates) and, for the e2e grader, the three.js checkout described next. The e2e tests serialise themselves on the one GPU; no --test-threads flag is needed for them, but sdf-text's three GPU gates still want -- --test-threads=1.

The viewer

cargo run --release --bin viewer -- --list
cargo run --release --bin viewer -- webgpu_lights_physical
cargo run --release --bin viewer -- 8                        # the same, by key
cargo run --release --bin viewer -- shadowmap --headless --frames 40

Opens the named example in a window (winit, tested on Wayland). Every graded example is there, and so are the ungraded webgpu_postprocessing_traa, webgpu_water, webgpu_postprocessing_sss, webgpu_postprocessing_ssgi, webgpu_postprocessing_ao, webgpu_postprocessing_dof, webgpu_postprocessing_ssr_denoise, webgpu_postprocessing_smaa, webgpu_postprocessing_pixel, webgpu_postprocessing_retro, webgpu_upscaling_taau and webgpu_upscaling_fsr1. The last five are ported but their e2e tests are #[ignore]d, because three itself fails their references on this machine; their smaa, pixel, retro taau and fsr1 notes have the numbers. Each one animates, orbits, dollies and pans through its own animate(), resize() and OrbitControls — the viewer drives the example, it does not restate it. --list prints the examples with their keys, and a key stands in for the name on the command line; in the window, [ and ] step to the previous and next example, because most of them do not fit in the 36 single keys a keyboard has. The window prints one line a second with the frame rate and the steady-state render time (mean and max over the last 60 frames, after a 10-frame warm-up):

webgpu_lights_phong — 1000x625 — 59.9 fps — render mean 1.61 ms max 1.79 ms (last 60 frames, after 10 warm-up)

--headless --frames N renders N frames with no window and reports the same numbers for the whole frame, CPU and GPU, which is how the table above was measured; add --screenshot out.png to keep the last frame. It also writes target/e2e/<example>/strip.png: three more frames tiled left to right at 1:1, each with its draw calls and its build counts in the gutter under it, so the count ladder sits beside the time one. The e2e harness writes the same strip per rung (steady-strip.png) and one for the geometry-mutation case.

Addons

three.js keeps its controls, loaders and helpers out of core, in examples/jsm/ — exported as three/addons/*, importing from three like any user, and without core's stability promise. addons/ is the same tier here: workspace crates that depend on three-rs and are not ports of anything in three.js' src/.

three_rs::addons::controls::OrbitControls is the exception to that rule, and it is in the root crate rather than a workspace one: most of the graded pages create an OrbitControls, and an example pulled in by #[path] cannot reach a crate that depends on three-rs. It is a port of examples/jsm/controls/OrbitControls.js — the same state, the same defaults, the same update() — but input-agnostic: it takes pointer_down, pointer_move, pointer_up, wheel and key as plain values, so the same controls serve a winit window, a DOM canvas and a test. It is graded the way everything else here is, against the original: tools/orbit_controls_reference.mjs runs the JS class under node over twelve scripted scenarios and prints the camera it ends with, and tests/addons_orbit_controls.rs replays the same scenarios and asserts every number to 1e-9. FirstPersonControls, FlyControls and TransformControls (the translate/rotate/scale gizmo) sit beside it in the same module and are graded the same way; docs/controls.md describes all four.

addons/controls/ is a different addon: three-rs-controls, a MapControls in the spirit of three.js' addon of that name, over a Ground — a sphere whose north pole is the world origin, so R = 1e7 is a plane and a small R a planet, with no separate case. The orbit target is a point on the ground, the camera never rolls — up is the ground normal — and every field is damped with smooth_damp, a port of camera-controls' smoothDamp.

cargo run --release -p three-rs-controls --bin heli is the demo, with its key map on screen: left-drag grabs the ground, right-drag (or ctrl-drag) orbits, the wheel zooms to the pointer, the arrows pan, [ ] curl the ground and P flattens it, Home resets, Tab is the overview (click a pane to drop onto it), Esc quits.

cargo run --release -p three-rs-controls --bin heli -- \
    --headless addons/controls/shots/heli-sphere-high.png \
    --radius 300 --pose 0,0,800,0,20 [--overview] [--size 1600x1000] [--no-legend]

Running the examples and the e2e grader

The examples load their textures and models from Three's own examples/ directory, and the grader reads Three's reference screenshots and runs its comparator under node. So they need a three.js checkout:

git clone https://github.com/mrdoob/three.js ~/src/vendor/three.js
git -C ~/src/vendor/three.js checkout 5f610f5   # the r187 tag, once upstream tags it
(cd ~/src/vendor/three.js && npm ci)
export THREE_JS_DIR=~/src/vendor/three.js     # this is the default location
cargo test --test e2e -- --nocapture

Each e2e test writes actual.png, the reference, and a diff strip under target/e2e/<example>/. The grader is unmodified; rung0/grader-flags.patch is only needed to run Three's own Chrome-based e2e suite on Linux with a real Vulkan adapter, which is how the reference numbers were calibrated (rung0/RUNG0.md). docs/grader.md says what the grader reproduces and what a pass means.

Two more checkouts are optional:

env var default needed by
D3_GALLERY_DIR ~/src/vendor/d3-gallery sdf-text's d3_treemap_labels example (its flare.json)
D3_THREE_DIR ~/src/projects/d3-three/main the image comparison in sdf-text's tests/d3_treemap_labels.rs (its examples/screenshots/d3_treemap.jpg)

Tests that need a checkout that is missing fail on the open with the path they looked for. The one exception is D3_THREE_DIR: d3-three is not public yet, so without it that test prints the path it looked for and skips only the image comparison.

Layout

src/            the three-rs crate, mirroring three.js's src/ tree
  math core cameras geometries lights loaders materials objects textures animation
  nodes/        TSL nodes and the WGSL NodeBuilder
  renderer/     the wgpu backend: pipelines, bindings, passes, shadows, present
  bin/viewer.rs
examples/       one file per ported Three example, also compiled into tests/e2e
tests/          Three's QUnit tests ported per module, plus the e2e harness
sdf-text/       workspace crate: SDF text and BatchedText, with its examples and gates
addons/         workspace crates in the role of three.js' examples/jsm: not ports
  controls/     three-rs-controls — the map camera, its ground, and the heli demo
docs/           design notes per subsystem and per-example progress logs
rung0/          how the grader was calibrated
tools/          dump-webgpu.mjs, the rung dump hook (see docs/dumping.md)

ARCHITECTURE.md is the map: one frame through the code, and which note explains each part. Of the notes, docs/nodes.md and docs/scene-graph.md are the two to read first: how the node system maps onto Three's, and how the Rc<RefCell<Object3D>> scene graph replaces Three's prototype tree. docs/dumping.md says how a rung captures the WGSL and GPU descriptors it ports against.

How it was built

Example by example. Each "rung" takes one Three example, dumps the WGSL Three generates for it (tools/dump-webgpu.mjs; see docs/dumping.md), ports whatever the example needs until the generated WGSL matches and the grader passes, then merges. The port was directed and largely written by Claude agents with the pixel diff as the ground truth; the per-rung progress notes in docs/ record what each rung found, including the places where Three's own output was reproduced bug-for-bug and the places where it deliberately was not.

License

MIT, in LICENSE. The portions translated from three.js are under three.js' own MIT licence, in LICENSE-three.js.

About

Three.js core and WebGPURenderer ported to Rust on wgpu, graded by Three's own e2e pixel diff

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages