Меблируем комнату. Часть 1: Динамическая загрузка
2014-08-15
Продолжая тему интерактивности в 3D-приложениях, рассмотрим реализацию приложения "Игровая комната", в котором пользователь сможет обставить комнату предметами мебели.
В этой статье цикла мы покажем, как использовать такие возможности движка, как динамическая загрузка и выгрузка объектов. Этот функционал может оказаться полезным для создания конфигураторов заказов, каталогов товаров или протяженных игровых локаций. В приложениях такого рода загрузка всех имеющихся ресурсов за один прием не рациональна или не возможна. Динамическая загрузка позволяет снизить количество занимаемой оперативной и видео-памяти, уменьшить объем сетевого трафика и значительно ускорить старт приложения.
Примечание
Рекомендуем также ознакомиться со статьей "Урок: создание интерактивного веб-приложения", в которой подробно описаны структура и принципы работы минимальной программы на основе Blend4Web.
Подготовка файлов сцен
Динамическая загрузка подразумевает, что в приложении уже имеется некоторая "основная" сцена (результат экспорта соответствующего blend-файла), после чего к ней в результате действий пользователя добавляется содержимое новых сцен (экспортированных из отдельных blend-файлов). В нашем приложении "основной" будет сцена с пустой комнатой, а дополнительные сцены будут содержать по одному объекту мебели.
О том, как были созданы графические ресурсы, мы расскажем в одной из следующих частей цикла.
Примечание
При динамической загрузке к основной сцене добавляются все объекты за исключением источников освещения и камер из вторичных сцен.
а) Основная сцена
На основной сцене мы разместим модель комнаты, а также установим глобальные настройки приложения.
При выборе объектов мебели будем подсвечивать их эффектом Outline. Цвет эффекта по умолчанию может быть установлен на панели Render > Object Outlining. Мы будем изменять его на красный программно для индикации коллизий с другими объектами.
Примечание
Динамически загружаемые сцены не влияют на настройки основной сцены, поэтому цвет эффекта Outline имеет смысл выставлять только в главном blend-файле.
На панели Render > Object Outlining и на панели Scene > Objects Selection выставим опцию Enable в положение ON. Это следует делать в случае, когда сцена изначально не содержит доступные для выбора и подсветки объекты, но потом они появятся на ней в результате динамической загрузки.
б) Дополнительные сцены
Разместим предварительно созданные модели мебели по одному в каждом blend-файле.
Выставим необходимые настройки на панели Object. Во-первых, включим опцию Rendering Properties > Force Dynamic Object, чтобы объект можно было перемещать в приложении. Во-вторых, включим опции Selection and Outlining > Selectable и Selection and Outlining > Enable Outlining для возможности выбирать и подсвечивать объект.
Все 3D-сцены, созданные на предыдущем этапе, экспортируем с помощью меню File > Export > Blend4Web (.json). В нашем случае экспортированные файлы находятся в директории blend_data/.
Инициализация приложения
Приведем код главного модуля приложения cartoon_interior.js целиком:
"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();
Как обычно, инициализация происходит с помощью функции init():
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]
});
};
Как было указано в предыдущем уроке, динамическое изменение размеров Canvas-элемента можно включить опцией autoresize при инициализации приложения. Это простой, но не самый оптимальный способ. В некоторых браузерах это может приводить к уменьшению FPS.
Для более сложных приложений рекомендуется использовать метод resize_to_container() из модуля container.js. Он приводит размеры элемента Canvas в соответствие с размерами его контейнера, определенного параметром canvas_container_id при инициализации приложения.
Для "реагирования" на поведение браузера используем обработчик события window.onresize:
function init_cb(canvas_elem, success) {
...
window.onresize = m_cont.resize_to_container;
m_cont.resize_to_container();
...
}
Таким образом, будет достаточно настроить CSS стили элемента-контейнера, например, как это делается по умолчанию:
div#main_canvas_container {
position: absolute;
width: 100%;
height: 100%;
left: 0;
top: 0;
}
В нашем примере Canvas всегда будет следовать размерами за элементом-контейнером main_canvas_container, занимающим все окно браузера.
Динамическая загрузка
Загрузка дополнительных сцен реализована в функции init_controls() и непосредственно связана с интерфейсом приложения - объекты мебели добавляются при нажатии пользователем соответствующей кнопки:
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);
});
...
}
Загрузка дополнительных сцен происходит с помощью метода data.load() - т.е. точно также, как и основной сцены. По завершении загрузки будет вызвана функция-обработчик loaded_cb().
Значение true последнего аргумента в методе data.load() означает, что новые объекты после загрузки будут скрыты (кроме того, эти объекты не будут участвовать в физической симуляции). Это понадобилось нам, чтобы переместить объекты загруженной сцены в нужное место перед тем, как их отобразить.
Точка, в которую будут помещаться новые объекты, была обозначена размещенным в нужном месте сцены пустым объектом ("EMPTY"). После загрузки основной сцены сохраняем координаты этого пустого объекта (названного "spawner") в соответствующей переменной:
...
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);
...
}
После загрузки дополнительной сцены подготавливаем объекты для отображения:
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);
}
}
Рассмотрим этот код подробнее.
При динамической загрузке происходит добавление объектов на основную сцену приложения. При этом каждая загруженная сцена имеет порядковый номер (0 - основная сцена, 1,2,3,... - последующие), который присваивается каждому объекту этой сцены.
Используя этот идентификатор, найдем вновь загруженные объекты:
var objs = m_scenes.get_all_objects("ALL", data_id);
Затем включаем для них физическую симуляцию:
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);
...
...и включаем отображение только объектов типа MESH:
...
// show appended object
if (m_obj.is_mesh(obj))
m_scenes.show_object(obj);
...
Выгрузка
При нажатии на кнопку удаления, выгрузим выбранный объект из приложения:
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;
}
});
...
}
Выгрузка осуществляется с помощью метода data.unload(). В метод можно передать порядковый номер сцены, объекты которой необходимо выгрузить. Вызов метода без аргументов или с нулевым аргументом выгрузит всю основную сцену.
Порядковый номер сцены можно узнать по любому принадлежащему ей объекту:
var id = m_scenes.get_object_data_id(_selected_obj);
В нашем случае каждый объект мебели находится в отдельной сцене, поэтому ее выгрузка равносильна удалению этого объекта из приложения.
Заключение
На этом о динамической загрузке всё. В следующей части мы подробно остановимся на настройках физики и взаимодействии пользователя с объектами.
Исходные файлы приложения и сцены включены в состав бесплатного дистрибутива Blend4Web SDK.
Запустить приложение в отдельном окнеИзменения
[2014-08-15] Изначальная публикация.
[2014-08-21] Внесены изменения в код приложения.
[2014-10-29] Обновлен код примера по причине изменения API.
[2014-12-23] Обновлен код примера по причине изменения API.
[2015-04-23] Правки текста об исходных файлах приложения.
[2015-05-13] Обновлен код примера по причине изменения API.
[2015-05-19] Внесены изменения в код приложения.
[2015-06-26] Внесены изменения в код приложения.
[2015-10-05] Небольшие правки текста.
[2016-08-22] Внесены изменения в код приложения.