The three.js library
func3d is built on three.js, the reference JavaScript library for 3D graphics in the browser, and on its camera control OrbitControls. Here is why it was chosen, how it is used in the site and which developments it opens up for research projects.
three.js in short
three.js wraps WebGL (the GPU-accelerated graphics API available in every browser) in an object scene: geometries, materials, lights, cameras. It is a mature open source project, with a very large ecosystem of add-ons and examples, and is the de facto standard for 3D on the web.
OrbitControls is the official add-on for orbital exploration: rotation around a target, wheel zoom, pan; it is the natural interaction model for data inspection (unlike the "first person" controls designed for games).
How they are used in the site
- Versions: since release r150 three.js ships ES modules only (no more global UMD build). The historical site stays on r128 (global build, used by the fluid dynamics and nuclear physics suite viewers), while func3d is isolated on r185 through an import‑map and a module bootstrap that publishes the library for the classic application chain. The two worlds coexist without conflicts.
- Fat lines (Line2/LineMaterial add-on): true line thickness for wireframes — plain WebGL draws 1-pixel lines.
- InstancedMesh: the cone tips of the vector field arrows are a single instanced mesh (one draw call for thousands of arrows).
- ShaderMaterial: point clouds use dedicated shaders with per-point colour and size (double encoding) and fading tails with per-vertex transparency (temporal trails).
- Parametric geometry: the (u,v) surfaces (spherical harmonics, Möbius, knotted tori...) are sampled grids turned into indexed meshes with uniform scaling — real proportions preserved.
- CanvasTexture: the filled contour map of the isolines is a texture generated on the fly from a 2D canvas and laid on the base plane.
- Colour management: recent three.js versions work in linear space and convert to sRGB on output; the whole func3d colour pipeline (colormaps, colorbars, bands) accounts for it so that scene and legends match.
- Line thickness: in WebGL the
linewidthattribute ofLineBasicMaterialis ignored — every line comes out one pixel wide, whatever value you write. It is a silent trap: the control exists, the value reaches the material, and nothing happens. The only way to get real thickness is to switch toLine2, which draws the line as geometry; that is why axes, grids, wireframes and curves use it whenever the requested width is greater than one. - View clipping: six
THREE.Planes form a box and are assigned to the materials (clippingPlanes, withlocalClippingEnabledon the renderer). Acting on materials means the clip updates without rebuilding the scene, and whatever must stay whole — axes and grids — is excluded simply by not assigning it to their materials. - Fat-line curves: parametric curves use the same Line2 add-ons, with one object per continuous stretch (non-finite points break the line) and per-vertex colour along the parameter. Two details cost you immediately if missed: the material resolution must be refreshed on every resize — for all stretches, or the line changes thickness in patches — and the instanced geometry must be freed by walking the group, because a group has neither geometry nor material of its own and the children's geometries would stay on the GPU at every rebuild.
- Image planes: a
PlaneGeometrywith a double-sidedMeshBasicMaterialon the registry texture. If the plane is semi-transparent, writing to the depth buffer is disabled: otherwise it hides what is behind it while still letting it show through, which is the classic defect of transparent planes. - Axis compass: a small secondary scene in a corner, with its own camera and renderer. A WebGL context is a limited resource (about fifteen per page), so it must be destroyed explicitly when the chart is rebuilt.
- Local libraries:
classi.js(classification: exact Jenks, quantiles, standard deviation, pretty numbers, logarithmic, fixed step),campionamento.js(base-point selection: one every k, FPS, k‑means++ with a reproducible generator),isolinee.js(contour levels and marching squares),formato.js(the numbers of tick and colorbar labels: a single formatter, where app.js used to have two that had already drifted apart),viste.js(the preset camera views, keyboard rotations and the table of keys) andpicking-griglia.js(raycasting surfaces on their grid) are UMD modules independent of the renderer, with headless test suites in Node: they can be exercised without opening a browser and reused in other pages. What only shows in the real page — the direction of the ticks, the renderers every rebuild must tear down, the computation in the Worker — has its own suites innode-tools/test/browser, which drive Chrome over the DevTools protocol. - Interpolation in a Web Worker: the algorithms that turn a dataset into a
surface (IDW, RBF, RST, kriging, natural neighbour, B-spline, bilinear, bicubic,
Lanczos) live in
interpolatori.js, another dependency-free UMD module. The page loads it, andinterpolatori-worker.jsloads it withimportScripts: the same code in both places, not a copy, so the computation done off the page thread gives the same values, bit for bit.interpolatori-client.jsdecides where to compute — right away if the estimate is under 60 ms (and the estimate corrects itself against the real timings of the machine), in the Worker if it is over, right away anyway if there is no Worker —, keeps the grids in a cache keyed on content (points, parameters, lattice: changing colour, opacity or scale recomputes nothing), stops withterminate()the computations overtaken by a new parameter, and shows progress and remaining time under the chart, with a button to stop. While the Worker runs, the previous surface stays and the page remains usable: kriging on the 121 ozone points, which froze it for 1.5 s at every rebuild with contour lines, now freezes it for about 30 ms. - Grid picking: three.js raycasting tests one triangle after another, and at
400 points per side a tooltip ray cost about twenty milliseconds, nearly a whole
frame. But a surface z = f(x,y) is a height field on a lattice, and a ray
can only hit the triangles of the cells its projection crosses:
picking-griglia.jsfollows it cell by cell, as in the grid traversal of Amanatides and Woo, and lets three test only those triangles, with its own code. The hits are identical to the bit, and a ray costs a tenth of a millisecond. - Preview while moving: on a very dense grid, while the time animation plays or the frame slider is dragged, the scene is redrawn on a grid reduced to 100 points per side, and becomes complete again as soon as you stop. Exports always start from the complete grid.
- Vector export: the scene can also be exported as SVG — axis labels,
tick values, title, legend frame and colorbars (the latter as
linearGradient, that is colours interpolated by the viewer rather than pixels), with the WebGL image as a raster background. Positions, sizes, families and colours are read from the computed style of the real elements, and the text baseline is asked of the browser instead of being estimated: the file matches what is on screen. With the Scene in vector form switch, three.js'sSVGRenderersteps in and the surface too comes out as polygons (one path per triangle, flat per-face shading): some ten megabytes, worth it when the drawing has to be retouched in an editor. The section profile, being a 2D drawing, comes out fully vector on its own.
Why it pays off
- GPU in the browser: dense grids, thousands of points and arrows stay interactive with nothing to install — it works on tablets too;
- reproducibility and sharing: a visualisation is a URL, not a software to install (the scene state travels in the link);
- ecosystem: controls, loaders (glTF, PLY...), abundant examples and documentation; skills easy to find;
- longevity: a project active for more than fifteen years, with a huge community and predictable backward compatibility management.
Possible developments in research projects
- WebGPU: three.js already offers a WebGPU renderer; for large datasets (millions of points, compute shaders for interpolation or particles) it will be the natural step once browser support consolidates.
- Volume rendering: 3D scalar fields (concentrations, densities) via ray‑marching on 3D textures — the natural complement of surfaces and clouds.
- Streamlines and particle tracing: integration of trajectories in vector fields (today shown as arrows), with interactive seeding.
- Large datasets: level‑of‑detail, progressive decimation and streaming of temporal frames from the backend (netCDF via the gateway) to overcome browser memory limits.
- WebXR: immersive inspection (VR/AR) of the scenes already built — with three.js it is an extension, not a rewrite.
- 3D GIS: for georeferenced data on the globe the dedicated tools remain preferable (CesiumJS, deck.gl); func3d covers the "Cartesian" space of laboratory and model analysis.
Keywords: three.js, WebGL, OrbitControls, 3D rendering, WebGPU, scientific visualisation