Export root geometry to .gltf


My name is Andreas Pappas and I’m working at CERN for the LHCb experiment.
I’m working on a modern visualization application for LHCb event data and geometry and currently I have a fulllhcb.root file ( the one that is used as well in the jsroot itself link)

I’ve already made 2 exporters, one to generate a .gltf file from the root one and one to generate a .json.gz ( three.js json format )

My problem is that my web browser tool to display the generated files does not render properly the json.gz one but it renders properly .gltf file. You can have a look here ~> link

So after all this introduction I wanted to ask if someone can help me on exporting multiple .gltf files from one root file. So for example for LHCb specifically I would like to generate multiple .gltf files and i.e. one for the velo, one for the magnet, one for the Ecalorimeter, Hcalorimeter and so on. So basically multiple .gltf files for each subdetector or even each geometry object from the root file itself maybe?

At this point I will provide the 2 exporters I already have, as well as the root file and the .gltf file

root to .json.gz exporter using node.js

let jsroot = require("jsroot");
let fs = require("fs");
var zlib = require('zlib');

console.log('JSROOT version', jsroot.version);

function exportGeometry(obj) {

    let opt = { numfaces: 100000, numnodes: 1000, wireframe: false };
    if (d.has("all")) {
        opt.numfaces *= 100;
        opt.numnodes *= 100;

    if (d.has("dflt") || d.has("dflt_colors"))
        opt.dflt_colors = true;

    let volumes = obj.fMasterVolume.fNodes.arr
    let top_volumes_names = []
    for (var i in volumes) {
        const name = volumes[i].fName
        console.log(i, name)

    let obj3d = jsroot.GEO.build(obj, opt);

    if (!obj3d) return;

    let data = obj3d.toJSON();

    data["info"] = []
    // add additional info to json
    for (var i in top_volumes_names) {
        const name = top_volumes_names[i]
        console.log(i, name, data["geometries"][i])

    const outputFileName = "old_LHCb_geometry.json";
    fs.writeFileSync(outputFileName, JSON.stringify(data));
    // save also compressed
    out = fs.createWriteStream(outputFileName + ".gz");
    var gzip = zlib.createGzip();
    const input = fs.createReadStream(outputFileName);

function main() {
    d = jsroot.decodeUrl();
        let filename = "./lhcbfull.root"
        if (filename.indexOf(".root") > 0) {
            // it is a ROOT file, must include a 'geo' key with the geometry (TGeoManager)
            let itemname = "Geometry;1";
                .then(file => file.readObject(itemname))


root to .gltf exporter


    <meta charset=utf-8>
    <title>Test display</title>
        body {
            margin: 0;

        canvas {
            width: 100%;
            height: 100%
    <script src="jsroot/node_modules/three/build/three.js"></script>
    <script src="jsroot/node_modules/three/examples/js/controls/OrbitControls.js"></script>
    <script src="GLTFExporter.js"> </script>
    <script src="jsroot/scripts/JSRootCore.js?geom&onload=init" type="text/javascript"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/1.3.8/FileSaver.js"></script>


    <div id="mylog" style="block"></div>

        var mylog = document.getElementById('mylog'); 
        var container, stats;
        var camera, scene, renderer, controls;

        var mouse = new THREE.Vector2(),
        var alllines = new THREE.Object3D();

        var rootgeom;
        var detector;

        var to_hide = ["HcalInstallationlvHcalInnerSupportFrame",
            "MagnetYoke", "ITlvAirVolume", "EcalModules", "HcalModules", "VeloVacTank", "VeloRFBox",
            "VeloRFFoil", "PipeSection", "Rich1lvRich1Mgs", "Rich1lvRich1MagSh", "Rich1lvRich1Exit",
            "Velo2Rich", "Rich2lvRich2MagSh", "Rich2lvRich2Tube"];

        var hide_children = ["HcalInstallationlvHcal_4825", "EcalInstallationEcal_4826"];

        function process_root_node(node, level = 0, path = "") {
            //mylog.innerHTML += path + " " +  level + " " + node.fName + "\n";

            tmpname = node.fName;
            iterate_children = true;
            //console.log("processing:", tmpname, path, node)

            for (let i = 0; i < to_hide.length; i++) {
                item = to_hide[i];
                //console.log("Checking:", item);
                if (tmpname.startsWith(item)) {
                    node.fVolume.fGeoAtt = 0;

            for (let i = 0; i < hide_children.length; i++) {
                item = hide_children[i];
                //console.log("Checking:", item);
                if (tmpname.startsWith(item)) {
                    node.fVolume.fGeoAtt = 4;
                    iterate_children = false;
                    //console.log("Matched", item, tmpname, path)

            //console.log("subvolume:", tmpname, node);
            if (node.fVolume.fNodes && iterate_children) {
                for (let j = 0; j < node.fVolume.fNodes.arr.length; j++) {
                    snode = node.fVolume.fNodes.arr[j];
                    process_root_node(snode, level + 1, path + "/" + tmpname);

        function add_geometry(obj) {

            // options for building three.js model
            var opt = {
                numfaces: 5000000,
                numnodes: 50000,
                dflt_colors: true,
                vislevel: 4
            rootgeom = obj;
            process_root_node(obj.fNodes.arr[0], 0, "");
            var obj3d = JSROOT.GEO.build(obj, opt, display_geometry);

        function display_geometry(obj3d) {

            console.log("DISPLAYING", obj3d);
            if (!obj3d) return;

            detector = obj3d;
            var box3 = new THREE.Box3().setFromObject(obj3d);
            geom_size = box3.getSize().length();
            camera.far = geom_size * 5;

        function loadgeo() {

            container = document.createElement('div');

            // renderer
            renderer = new THREE.WebGLRenderer();
            renderer.setSize(window.innerWidth, window.innerHeight);

            renderer.sortObjects = false;

            var info = document.createElement('div');
            info.style.position = 'absolute';
            info.style.top = '10px';
            info.style.width = '100%';
            info.style.textAlign = 'center';

            // camera
            camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 1, 1000);
            camera.position.set(-10000, 1000, 3000);
            //camera.up.set(0.0, 1.0, 0.0);

            // controls
            controls = new THREE.OrbitControls(camera, renderer.domElement);
            controls.target.set(0.0, 0.0, 8);

            // scene
            scene = new THREE.Scene();

            var filename = "lhcbfull.root"
            JSROOT.OpenFile(filename, function (file) {
                file.ReadObject("Geometry;1", add_geometry);



        function postinit() {

            // Particles
            // line 1
            var linegeometry1 = new THREE.Geometry();
            var line1 = new THREE.Line(linegeometry1, new THREE.LineBasicMaterial({
                color: 0x00033
            linegeometry1.vertices.push(new THREE.Vector3(0, 0, -1500));
            linegeometry1.vertices.push(new THREE.Vector3(1000, 1000, 15000));

            // line 2
            var linegeometry2 = new THREE.Geometry();
            var line2 = new THREE.Line(linegeometry2, new THREE.LineBasicMaterial({
                color: 0x00033
            linegeometry2.vertices.push(new THREE.Vector3(0, 0, -1500));
            linegeometry2.vertices.push(new THREE.Vector3(-1000, 1000, 15000));

            //line 3
            var linegeometry3 = new THREE.Geometry();
            var line3 = new THREE.Line(linegeometry3, new THREE.LineBasicMaterial({
                color: 0x00033
            linegeometry3.vertices.push(new THREE.Vector3(0, 0, -1500));
            linegeometry3.vertices.push(new THREE.Vector3(-1000, -1000, 15000));


            scene.add(new THREE.AmbientLight(0xff0000, 0.8));
            var light = new THREE.DirectionalLight(0x00ff00, 1);
            light.position.set(-100, 4000, -10000); //.normalize();

            window.addEventListener('resize', onWindowResize, false);

            var doexport = true;
            if (doexport) {
                options = {};
                GLTFExporter.parse(detector, function (gltf) {

                    var fileToSave = new Blob([JSON.stringify(gltf)], {
                        type: 'application/json',
                        name: "lhcb.gltf"

                    // Save the file
                    saveAs(fileToSave, "lhcb.gltf");
                }, options);

        function animate() {

        function render() {
            renderer.render(scene, camera);

        function onWindowResize() {
            camera.aspect = window.innerWidth / window.innerHeight;
            renderer.setSize(window.innerWidth, window.innerHeight);



        function init() {
            console.log("In init");

It would be ideal if you could help me also modify somehow the .gltf exporter to use it with node.js and not as a javascript application in the browser since I want only to use it to generate the multiple .gltf files from the single give root file.

Thank you very much in advance for your time and effort on this!

Best regards,

Andreas Pappas

P.S. Here you can file the root file mentioned. Unfortunately I'm not able to upload the generated .gltf file because it is too large. But if you use the exporter mentioned above and the following root file then the .gltf will be generated properly.
lhcbfull.root (328.2 KB)

Hi Andreas,

That are the problems with JSON export?
Did you check that content of produced .json.gz file looks reasonable?
Are you using same three.js version when trying import produced JSON file?

I have no experience with .gltf exporter, but my guess is that it also should work with node.js.
Just check google, there are many examples like this


Hi Linev,

Sorry for my late response but I’m working on too many things at the same time…

I came to the conclusion to drop completely the JSON export because three.js will completely drop the support on .json geometry loading files for various reasons they have addressed into their next release.

Thus, because of that I will focus into working only with the gltf exporter as a node.js module.
The thing is I still want to try and achieve exporting into multiple .gltf files the different pieces or objects if you want of the geometry inside the single .root file. Because my current exporter just exports into one .gltf file and that is the whole geometry from the root file itself.

Another question I have is that I would like to try and make a root importer directly to my tool as you’ve probably already done with jsroot. That is to import directly a root file and behind the scenes to load and render it with three.js into the browser and ideally to have it separated into multiple objects once again, so I can have the option to enable / disable which parts / objects I want to be rendered on the browser via a menu. If I can achieve that somehow and the performance is reasonable, then maybe I can skip the .gltf exporter. Maybe you have some more experience on that part and you could help me there?

Thank you once again for your response.

Best regards,

Andreas Pappas

Hi Andreas,

Sorry, I cannot help with gtlf exporter - no idea how it works and how multiple parts can be exported there.

three.js model, build by JSROOT, fully reproduces hierarchy of TGeo volumes. Means it should be straight-forward to enable/disable rendering of geometry parts by just setting visible flag for any part of the model. To identify different parts, you could use name attribute of THREE.Object3D, produced by JSROOT.


And seems to be, you are using older version 5 of JSROOT.
There is version 6.1 already and I really recommend to use it - new features will be implemented only there.

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.