Blog

Furnishing a Room Part 1: Dynamic Loading

2014-08-15

Proceeding with interactivity in 3D apps, we'll look at the implementation of the "Playroom" application, in which the user will get the possibility to furnish the room with furniture items.

In this first-of-a-series article we'll show how to use the engine functionality such as dynamic loading and unloading of objects. These features may be useful for creating sales configurators, catalogues and extensive game levels. In such applications the loading of all available resources at the same time is impractical or impossible. Loading the required resources at run-time decreases memory consumption (both RAM and VRAM), reduces traffic and significantly speeds up the application launch.

Note

We also recommend to read our past Tutorial: Creating an Interactive Web Application to familiarize yourself with the design principles of a Blend4Web application.

Preparing the Scene Files

Dynamic loading implies that the app already has some "main" scene (exported from the corresponding blend file). Then as a result of the user's actions new content from some other scenes is added to it (which in turn were exported from the separate blend files). In our app the scene with the empty room is considered the "main" one while the additional scenes will contain a single furniture item each.

We'll speak about creating the graphics content in one of our next articles.

Note

When loading dynamically, all objects except light sources and cameras are added from the additional scenes to the main scene.

a) The main scene

We'll place the room model and set up global application settings in the main scene.

At selecting the objects we'll light them up with an outline effect. The default outline color can be set up on the Render > Object Outlining panel. We'll change it to red in our app to indicate collisions with other objects.

Note

Dynamically loaded scenes don't affect the main scene settings and so the use of the outline color option makes sense only in the main blend file.

Also, we should set Render > Object Outlining and Scene > Objects Selection options to ON on the corresponding panels. This should be done if a scene doesn't contain pickable and outlineable objects initially, but they would be added to the scene through dynamic loading.


b) The additional scenes

Let's place the furniture models (created beforehand) each into a separate blend file.

Let's set up the required settings on the Object panel. First, enable Rendering Properties > Force Dynamic Object to make the object movement possible. Second, enable the Selection and Outlining > Selectable option and the Selection and Outlining > Enable Outlining option to be able to pick and highlight this object.

Let's export all the 3D scenes created on the previous step using the File > Export > Blend4Web (.json) menu. In our case the exported files are saved to the blend_data/ directory.

App Initialization

The code of the main app module cartoon_interior.js is listed in whole:

"use strict";

b4w.register("cartoon_interior", function(exports, require) {

var m_app       = require("app");
var m_cam       = require("camera");
var m_cont      = require("container");
var m_ctl       = require("controls");
var m_data      = require("data");
var m_mouse     = require("mouse");
var m_math      = require("math");
var m_obj       = require("objects");
var m_phy       = require("physics");
var m_preloader = require("preloader");
var m_scenes    = require("scenes");
var m_trans     = require("transform");
var m_util      = require("util");

var m_quat = require("quat");

var OUTLINE_COLOR_VALID = [0, 1, 0];
var OUTLINE_COLOR_ERROR = [1, 0, 0];
var FLOOR_PLANE_NORMAL = [0, 1, 0];

var ROT_ANGLE = Math.PI/4;

var WALL_X_MAX = 4;
var WALL_X_MIN = -3.8;
var WALL_Z_MAX = 4.2;
var WALL_Z_MIN = -3.5;

var _obj_delta_xy = new Float32Array(2);
var spawner_pos = new Float32Array(3);
var _vec3_tmp = new Float32Array(3);
var _vec3_tmp2 = new Float32Array(3);
var _vec3_tmp3 = new Float32Array(3);
var _vec4_tmp = new Float32Array(4);
var _pline_tmp = m_math.create_pline();

var _drag_mode = false;
var _enable_camera_controls = true;

var _selected_obj = null;

exports.init = function() {
    m_app.init({
        canvas_container_id: "main_canvas_container",
        callback: init_cb,
        physics_enabled: true,
        alpha: false,
        background_color: [1.0, 1.0, 1.0, 0.0]
    });
};

function init_cb(canvas_elem, success) {

    if (!success) {
        console.log("b4w init failure");
        return;
    }

    m_preloader.create_preloader();

    canvas_elem.addEventListener("mousedown", main_canvas_down);
    canvas_elem.addEventListener("touchstart", main_canvas_down);

    canvas_elem.addEventListener("mouseup", main_canvas_up);
    canvas_elem.addEventListener("touchend", main_canvas_up);

    canvas_elem.addEventListener("mousemove", main_canvas_move);
    canvas_elem.addEventListener("touchmove", main_canvas_move);

    window.onresize = m_cont.resize_to_container;
    m_cont.resize_to_container();
    load();
}

function preloader_cb(percentage) {
    m_preloader.update_preloader(percentage);
}

function load() {
    m_data.load("blend_data/environment.json", load_cb, preloader_cb);
}

function load_cb(data_id) {
    m_app.enable_camera_controls(false, false, false, m_cont.get_canvas());
    init_controls();

    var spawner = m_scenes.get_object_by_name("spawner");
    m_trans.get_translation(spawner, spawner_pos);
}

function init_controls() {
    var controls_elem = document.getElementById("controls-container");
    controls_elem.style.display = "block";

    init_buttons();

    document.getElementById("load-1").addEventListener("click", function(e) {
        m_data.load("blend_data/bed.json", loaded_cb, null, null, true);
    });
    document.getElementById("load-2").addEventListener("click", function(e) {
        m_data.load("blend_data/chair.json", loaded_cb, null, null, true);
    });
    document.getElementById("load-3").addEventListener("click", function(e) {
        m_data.load("blend_data/commode_and_pot.json", loaded_cb, null, null, true);
    });
    document.getElementById("load-4").addEventListener("click", function(e) {
        m_data.load("blend_data/fan.json", loaded_cb, null, null, true);
    });
    document.getElementById("load-5").addEventListener("click", function(e) {
        m_data.load("blend_data/table.json", loaded_cb, null, null, true);
    });

    document.getElementById("delete").addEventListener("click", function(e) {
        if (_selected_obj) {
            var id = m_scenes.get_object_data_id(_selected_obj);
            m_data.unload(id);
            _selected_obj = null;
        }
    });
    document.getElementById("rot-ccw").addEventListener("click", function(e) {
        if (_selected_obj)
            rotate_object(_selected_obj, ROT_ANGLE);
    });
    document.getElementById("rot-cw").addEventListener("click", function(e) {
        if (_selected_obj)
            rotate_object(_selected_obj, -ROT_ANGLE);
    });
}

function init_buttons() {
    var ids = ["delete", "rot-ccw", "rot-cw"];

    for (var i = 0; i < ids.length; i++) {
        var id = ids[i];

        document.getElementById(id).addEventListener("mousedown", function(e) {
            var parent = e.target.parentNode;
            parent.classList.add("active");
        });
        document.getElementById(id).addEventListener("mouseup", function(e) {
            var parent = e.target.parentNode;
            parent.classList.remove("active");
        });
        document.getElementById(id).addEventListener("touchstart", function(e) {
            var parent = e.target.parentNode;
            parent.classList.add("active");
        });
        document.getElementById(id).addEventListener("touchend", function(e) {
            var parent = e.target.parentNode;
            parent.classList.remove("active");
        });
    }
}

function loaded_cb(data_id) {

    var objs = m_scenes.get_all_objects("ALL", data_id);
    for (var i = 0; i < objs.length; i++) {
        var obj = objs[i];

        if (m_phy.has_physics(obj)) {
            m_phy.enable_simulation(obj);

            // create sensors to detect collisions
            var sensor_col = m_ctl.create_collision_sensor(obj, "FURNITURE");
            var sensor_sel = m_ctl.create_selection_sensor(obj, true);

            if (obj == _selected_obj)
                m_ctl.set_custom_sensor(sensor_sel, 1);

            m_ctl.create_sensor_manifold(obj, "COLLISION", m_ctl.CT_CONTINUOUS, 
                    [sensor_col, sensor_sel], logic_func, trigger_outline);


            // spawn appended object at a certain position
            var obj_parent = m_obj.get_parent(obj);
            if (obj_parent && m_obj.is_armature(obj_parent))
                // translate the parent (armature) of the animated object
                m_trans.set_translation_v(obj_parent, spawner_pos);
            else
                m_trans.set_translation_v(obj, spawner_pos);
        }

        // show appended object
        if (m_obj.is_mesh(obj))
            m_scenes.show_object(obj);
    }
}

function logic_func(s) {
    return s[1];
}

function trigger_outline(obj, id, pulse) {
    if (pulse == 1) {
        // change outline color according to the  
        // first manifold sensor (collision sensor) status
        var has_collision = m_ctl.get_sensor_value(obj, id, 0);
        if (has_collision)
            m_scenes.set_outline_color(OUTLINE_COLOR_ERROR);
        else
            m_scenes.set_outline_color(OUTLINE_COLOR_VALID);
    }
}

function rotate_object(obj, angle) {
    var obj_parent = m_obj.get_parent(obj);
    
    if (obj_parent && m_obj.is_armature(obj_parent)) {
        // rotate the parent (armature) of the animated object
        var obj_quat = m_trans.get_rotation(obj_parent, _vec4_tmp);
        m_quat.rotateY(obj_quat, angle, obj_quat);
        m_trans.set_rotation_v(obj_parent, obj_quat);
    } else {
        var obj_quat = m_trans.get_rotation(obj, _vec4_tmp);
        m_quat.rotateY(obj_quat, angle, obj_quat);
        m_trans.set_rotation_v(obj, obj_quat);
    }
    limit_object_position(obj);
}

function main_canvas_down(e) {
    _drag_mode = true;

    if (e.preventDefault)
        e.preventDefault();

    var x = m_mouse.get_coords_x(e);
    var y = m_mouse.get_coords_y(e);

    var obj = m_scenes.pick_object(x, y);

    // handling outline effect
    if (_selected_obj != obj) {
        if (_selected_obj)
            m_scenes.clear_outline_anim(_selected_obj);
        if (obj)
            m_scenes.apply_outline_anim(obj, 1, 1, 0);

        _selected_obj = obj;
    }

    // calculate delta in viewport coordinates
    if (_selected_obj) {
        var cam = m_scenes.get_active_camera();

        var obj_parent = m_obj.get_parent(_selected_obj);
        if (obj_parent && m_obj.is_armature(obj_parent))
            // get translation from the parent (armature) of the animated object
            m_trans.get_translation(obj_parent, _vec3_tmp);
        else
            m_trans.get_translation(_selected_obj, _vec3_tmp);
        m_cam.project_point(cam, _vec3_tmp, _obj_delta_xy);

        _obj_delta_xy[0] = x - _obj_delta_xy[0];
        _obj_delta_xy[1] = y - _obj_delta_xy[1];
    }
}

function main_canvas_up(e) {
    _drag_mode = false;
    // enable camera controls after releasing the object
    if (!_enable_camera_controls) {
        m_app.enable_camera_controls();
        _enable_camera_controls = true;
    }
}

function main_canvas_move(e) {
    if (_drag_mode)
        if (_selected_obj) {
            // disable camera controls while moving the object
            if (_enable_camera_controls) {
                m_app.disable_camera_controls();
                _enable_camera_controls = false;
            }

            // calculate viewport coordinates
            var cam = m_scenes.get_active_camera();

            var x = m_mouse.get_coords_x(e);
            var y = m_mouse.get_coords_y(e);

            if (x >= 0 && y >= 0) {
                x -= _obj_delta_xy[0];
                y -= _obj_delta_xy[1];

                // emit ray from the camera
                var pline = m_cam.calc_ray(cam, x, y, _pline_tmp);
                var camera_ray = m_math.get_pline_directional_vec(pline, _vec3_tmp);

                // calculate ray/floor_plane intersection point
                var cam_trans = m_trans.get_translation(cam, _vec3_tmp2);
                m_math.set_pline_initial_point(_pline_tmp, cam_trans);
                m_math.set_pline_directional_vec(_pline_tmp, camera_ray);
                var point = m_math.line_plane_intersect(FLOOR_PLANE_NORMAL, 0,
                        _pline_tmp, _vec3_tmp3);

                // do not process the parallel case and intersections behind the camera
                if (point && camera_ray[1] < 0) {
                    var obj_parent = m_obj.get_parent(_selected_obj);
                    if (obj_parent && m_obj.is_armature(obj_parent))
                        // translate the parent (armature) of the animated object
                        m_trans.set_translation_v(obj_parent, point);
                    else
                        m_trans.set_translation_v(_selected_obj, point);
                    limit_object_position(_selected_obj);
                }
            }
        }
}

function limit_object_position(obj) {
    var bb = m_trans.get_object_bounding_box(obj);

    var obj_parent = m_obj.get_parent(obj);
    if (obj_parent && m_obj.is_armature(obj_parent))
        // get translation from the parent (armature) of the animated object
        var obj_pos = m_trans.get_translation(obj_parent, _vec3_tmp);
    else
        var obj_pos = m_trans.get_translation(obj, _vec3_tmp);

    if (bb.max_x > WALL_X_MAX)
        obj_pos[0] -= bb.max_x - WALL_X_MAX;
    else if (bb.min_x < WALL_X_MIN)
        obj_pos[0] += WALL_X_MIN - bb.min_x;

    if (bb.max_z > WALL_Z_MAX)
        obj_pos[2] -= bb.max_z - WALL_Z_MAX;
    else if (bb.min_z < WALL_Z_MIN)
        obj_pos[2] += WALL_Z_MIN - bb.min_z;

    if (obj_parent && m_obj.is_armature(obj_parent))
        // translate the parent (armature) of the animated object
        m_trans.set_translation_v(obj_parent, obj_pos);
    else
        m_trans.set_translation_v(obj, obj_pos);
}

});

b4w.require("cartoon_interior").init();

As usual the initialization is performed with the init() function:

exports.init = function() {
    m_app.init({
        canvas_container_id: "main_canvas_container",
        callback: init_cb,
        physics_enabled: true,
        alpha: false,
        background_color: [1.0, 1.0, 1.0, 0.0]
    });
};

As it was mentioned in the previous tutorial, we could use the autoresize option at the initialization stage in order to enable dynamic resolution for the Canvas. This solution is simple but not so optimal. It can lead to FPS dropping in some browsers.

That's why we recommended you to use the resize_to_container() method from the container.js module for more complex applications. It modifies the size of the Canvas element according to the size of its container. The container element's id can be defined by using the canvas_container_id parameter upon app initialization.

In order to detect changes of the browser window size, we'll use the window.onresize event callback:

function init_cb(canvas_elem, success) {
    ...
    window.onresize = m_cont.resize_to_container;
    m_cont.resize_to_container();
    ...
}

So, it would be enough to adjust CSS styles for the container, for example, how it was done by default:

div#main_canvas_container {
    position: absolute;
    width: 100%;
    height: 100%;
    left: 0;
    top: 0;
}

In our example the Canvas will always follow the dimensions of the main_canvas_container element, which occupies the whole browser window.


Dynamic Loading

The additional scenes are set up to be loaded inside init_controls(). This logic is hooked to the app interface so the furniture items are added when the user clicks on the corresponding button:

function init_controls() {
    ...
    document.getElementById("load-1").addEventListener("click", function(e) {
        m_data.load("blend_data/bed.json", loaded_cb, null, null, true);
    });
    document.getElementById("load-2").addEventListener("click", function(e) {
        m_data.load("blend_data/chair.json", loaded_cb, null, null, true);
    });
    document.getElementById("load-3").addEventListener("click", function(e) {
        m_data.load("blend_data/commode_and_pot.json", loaded_cb, null, null, true);
    });
    document.getElementById("load-4").addEventListener("click", function(e) {
        m_data.load("blend_data/fan.json", loaded_cb, null, null, true);
    });
    document.getElementById("load-5").addEventListener("click", function(e) {
        m_data.load("blend_data/table.json", loaded_cb, null, null, true);
    });
    ...
}

The additional scenes are loaded using data.load() i.e. using the same method as for the main scene. When the loading is finished the loaded_cb() callback is executed.

The last argument of data.load() is true. This means that the new objects will be hidden after they are loaded (also the physics for these objects is not simulated). We needed that in order to move the loaded objects into the right place before displaying them.

The point at which the new objects will be positioned is designated by an EMPTY object placed in the desired place of the scene. After the main scene is loaded we save the position of this empty object (named "spawner") to the corresponding variable:

...
var spawner_pos = new Float32Array(3);
...

function load_cb(data_id) {
    ...
    var spawner = m_scenes.get_object_by_name("spawner");
    m_trans.get_translation(spawner, spawner_pos);
    ...
}

After an additional scene is loaded we prepare the objects for displaying:

function loaded_cb(data_id) {

    var objs = m_scenes.get_all_objects("ALL", data_id);
    for (var i = 0; i < objs.length; i++) {
        var obj = objs[i];

        if (m_phy.has_physics(obj)) {
            m_phy.enable_simulation(obj);

            ...

            // spawn appended object at a certain position
            var obj_parent = m_obj.get_parent(obj);
            if (obj_parent && m_obj.is_armature(obj_parent))
                // translate the parent (armature) of the animated object
                m_trans.set_translation_v(obj_parent, spawner_pos);
            else
                m_trans.set_translation_v(obj, spawner_pos);
        }

        // show appended object
        if (m_obj.is_mesh(obj))
            m_scenes.show_object(obj);
    }
}

Let's look at this code in detail.

Dynamic loading results in adding the objects to the app main scene. During this every loaded scene receives an id number (0 - main scene, 1,2,3,... - subsequent ones). This id number is assigned to every object of this scene as well.

Let's find the loaded objects using this id:

var objs = m_scenes.get_all_objects("ALL", data_id);

Then we enable physics simulation for these objects:

m_phy.enable_simulation(obj);

...move the object to the desired position:

...
// spawn appended object at a certain position
var obj_parent = m_obj.get_parent(obj);
if (obj_parent && m_obj.is_armature(obj_parent))
    // translate the parent (armature) of the animated object
    m_trans.set_translation_v(obj_parent, spawner_pos);
else
    m_trans.set_translation_v(obj, spawner_pos);
...

...and display only MESH objects on the screen:

...
// show appended object
if (m_obj.is_mesh(obj))
    m_scenes.show_object(obj);
...

Unloading

When the delete button is pressed we unload the selected object from our app:

function init_controls() {
    ...
    document.getElementById("delete").addEventListener("click", function(e) {
        if (_selected_obj) {
            var id = m_scenes.get_object_data_id(_selected_obj);
            m_data.unload(id);
            _selected_obj = null;
        }
    });
    ...
}

Unloading is performed with data.unload(). The id of the scene, the objects of which we need to unload, can be passed to this method as an argument. Calling this method without arguments or with a zero argument will unload the whole main scene.

You can find out the scene's id by checking any of its objects:

var id = m_scenes.get_object_data_id(_selected_obj);

In our case every furniture item is placed into a separate scene each so that the unloading of the scene is equivalent to removing this object from the app.

Conclusion

That's all concerning dynamic loading. In the next article we'll look in detail at the physics setup and the user-object interaction.

The source files for this example are available as part of the Blend4Web SDK free distribution.

Run the application in a new window

Changelog

[2014-08-15] Initial release.

[2014-08-21] Updated app code.

[2014-10-29] Updated the example code because of API change.

[2014-12-23] Updated the example code because of API change.

[2015-04-23] Changes in text about the application source files.

[2015-05-13] Updated the example code because of API change.

[2015-05-19] Updated app code.

[2015-06-26] Updated app code.

[2015-10-05] Minor changes in the text.

[2016-08-22] Updated app code.