Model Aggregation via XML

One of the stand-out features of HOOPS Communicator is its extensive support for shattered workflows, meaning the ability to freely and efficiently aggregate models, either during authoring or during runtime. This can be either done with multiple loadSubtreeFromModel calls for every stream cache model a user wants to add to the scene or more efficiently via an XML file that describes the complete product structure of a model consisting of individual stream cache models down to the part level.

The general structure of this XML file is explained in our documentation here but the docs are a bit vague when it comes to explaining how to programmatically create this XML data. On top of that the “syntax” of this file is somewhat tricky to parse.

This forum-post should serve as a quick “real-world” example of on how to generate this data, with the specific use-case being to “serialize” a list of models that have been aggregated in the client. We are using this approach internally for some of our HOOPS Communicator demos to combine federated building models. In most real-world applications though you will build this XML data mostly server-side. We will dive into those more advanced scenarios in another forum-post.

Let’s start by aggregating a few models. The key for this example is to put them all under the same node. You can write code to do this yourself or use the small function below.

async function loadMultipleModels(models) {
        let startnode = hwv.model.createNode(hwv.model.getRootNode());
        for (let i = 0; i < models.length; i++) {
            let model = models[i].split(".")[0];
            let newnode = hwv.model.createNode(startnode, model);
            await hwv.model.loadSubtreeFromScsFile(newnode, models[i]);
        return startnode;

var startnode  = loadMultipleModels(["microengine.scs", "arboleda.scs","microengine.scs"])

If you run this code from the console in our standard demo viewer you will see that it loads all three models (with microengine being referenced twice). It also creates a node for each model with its name corresponding to the model name:

You can now select the top level node of each model from the model tree and start moving those models around using our handles (maybe use the scale operator from another forum post to resize the microengine models). After you are satisfied with how those models are arranged you can then call the provided function below that will generate the XML describing the structure of the top-level node we passed in.

copy(await generateShatteredXMLFromNode("combined",startnode))

If you call this function from the console you can just copy the resulting XML buffer and manually paste it into a file that needs to be accessible from the webserver (if you are using our demo viewer simply place it into the “web_viewer/src” directory). Of course you can also push this data to your server.

This XML file will include everything required to load those models again, including the matrices that define the relative placement of those models. The code also generates the correct object space bounding boxes for every model “prototype”. The function that derives this bounding box from the world space bounding returned by HOOPS Communicator is also provided.

Just to be clear, this example will generate a single hierarchy level and not the complete product structure of all the aggregated models. That data is still stored in the Stream Cache files of the models itself and will be available after loading.

After you have created this XML file we can reload the viewer (start with an empty scene) and then just load it with the code below. Make sure to turn off unit scaling so the models don’t get resized during loading. You should do this after the modelstructureready callback has triggered:


Again, this is a fairly simple example. You can of course continue to just load those files manually using individual loadSubtreeFromModel calls. It is important to understand however that in particular with server-side streaming using the aggregated XML instead of a series of loadSubtree call has distinct performance advantages as the streaming server can determine the correct “order” of streaming based on all the models referenced in the XML. It is also worth noting that you can use the same XML to generate a master stream cache model using converter that will load even faster.

If you have any questions or comments, please don’t hesitate to post them here in the forum.

async function getObjectBounding(nodeid) {
    let bounds = await hwv.model.getNodesBounding([nodeid]);
    let matrix = Communicator.Matrix.inverse(hwv.model.getNodeNetMatrix(nodeid));
    let boxcorners = bounds.getCorners();
    let boxtransformed = [];
    matrix.transformArray(boxcorners, boxtransformed);

    let outbounds = new Communicator.Box();
    outbounds.min = boxtransformed[0].copy();
    outbounds.max = boxtransformed[0].copy();
    for (let i=1;i<boxtransformed.length;i++)
    return outbounds;

 async function generateShatteredXMLFromNode(name, parentnode) {     

    let polist = [];
    let instancehash = [];

    let currentid = 2;
    let pochildrentext = "";
    let children = hwv.model.getNodeChildren(parentnode);

    for (let i = 0; i < children.length; i++) {
        let nodeid = children[i];
        let nodename = hwv.model.getNodeName(nodeid);

        let instance = instancehash[nodename];
        pochildrentext += currentid + " ";  
        if (instance == undefined) {
            let instanceRefId = currentid + 1;
            polist.push({ nodeid: nodeid, id: currentid, instanceref: instanceRefId});
            polist.push({ nodeid: nodeid, id: instanceRefId });
            instancehash[nodename] = { id: instanceRefId, count: 0 };
            polist.push({ nodeid: nodeid, id: currentid, instanceref: });

    let xml = "";        
    xml += '<!--HC 22.0.0-->\n';
    xml += '<Root>\n';
    xml += '<ModelFile>\n';
    xml += '<ProductOccurence Id="0" Name="' + name + '" Behaviour="1" Children="1" IsPart="false" Unit="1000.000000"/>\n';        
    xml += '<ProductOccurence Id="1" Name="' + name + ':Master" ExchangeId="" LayerId="65535" Style="65535" Behaviour="1" FilePath="" Children="' + pochildrentext + '" IsPart="false"/>\n';

    for (let i = 0; i < polist.length; i++) {
        let nodename = hwv.model.getNodeName(polist[i].nodeid);            
        if (polist[i].instanceref != undefined) {
            let instance = instancehash[nodename];
            xml += '<ProductOccurence Id="' + polist[i].id + '" Name="' + nodename + ":" + (instance.count++) + '" ExchangeId="" LayerId="65535" Style="65535" Behaviour="1" FilePath="" InstanceRef="' + + '" IsPart="false">\n';
            let matrix = hwv.model.getNodeMatrix(polist[i].nodeid);
            xml += '<Transformation RelativeTransfo="' + matrix.m[0] + " " + matrix.m[1] + " " + matrix.m[2] + " " + matrix.m[3] + " " + matrix.m[4] + " " + matrix.m[5] + " " + matrix.m[6] + " " + matrix.m[7] + " " + 
                matrix.m[8] + " " + matrix.m[9] + " " + matrix.m[10] + " " + matrix.m[11] + " " + matrix.m[12] + " " + matrix.m[13] + " " + matrix.m[14] + " " + matrix.m[15] + '"/>\n';
            xml += '</ProductOccurence>' + "\n";
        else {
            xml += '<ProductOccurence Id="' + polist[i].id + '" Name="" ExchangeId="" Behaviour="1" IsPart="false">' + "\n";
            xml += '<ExternalModel Name="' + nodename + '" Unit="1000.000000">' + "\n";

            let bounding = await getObjectBounding(polist[i].nodeid);
            xml += '<BoundingBox Min="' + bounding.min.x + " " + bounding.min.y + " " + bounding.min.z + '" Max="' + bounding.max.x + " " + bounding.max.y + " " + bounding.max.z + '"/>\n';
            xml += '</ExternalModel>\n';
            xml += '</ProductOccurence>\n';
    xml += '</ModelFile>\n';
    xml += '</Root>\n';
    return xml;