Меблируем комнату. Часть 2: Интерактивность и физика
2014-08-22
Продолжаем описание приложения "Игровая комната". В предыдущей статье цикла была рассмотрена технология динамической загрузки. Теперь же мы уделим внимание таким важным компонентам приложения как физика и система управления.
Настройка физики
Напомним, что для приложения были подготовлены несколько сцен: главная сцена, содержащая комнату, и дополнительные ‒ с объектами мебели.
Объекты мебели будут физически взаимодействовать между собой, но довольно простым образом - сигнализировать, если появится пересечение с другим таким же объектом.
Для этого в главной сцене необходимо разрешить физическую симуляцию, а именно, во вкладке
Что касается дополнительных сцен, то для каждого объекта мебели нужно настроить физические свойства, т.к. мы хотим отслеживать их пересечения друг с другом. Это можно сделать во вкладке Physics.
На панели
При выборе типа физического объекта
Далее включим опцию
Для каждого объекта выберем наиболее подходящий тип ограничивающего объема
Более подробно настройки физики описаны в соответствующем разделе документации.
Интерактивность: повороты
В приложении "Игровая комната" у пользователя есть возможность расставлять объекты мебели, т.е. поворачивать их и перемещать в пределах комнаты.
Вращение объекта мебели происходит по нажатию соответствующих кнопок в интерфейсе приложения:
function init_controls() {
...
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);
});
...
}
За реализацию отвечает функция rotate_object(). В ней используется кватернион вращения объекта, который мы поворачиваем на угол angle вокруг вертикальной оси и вновь присваиваем объекту:
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 init_cb(canvas_elem, success) {
...
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);
...
}
При нажатии вызовется обработчик main_canvas_down(). Здесь мы получаем экранные координаты точки нажатия и определяем выделенный объект:
function main_canvas_down(e) {
...
var x = m_mouse.get_coords_x(e);
var y = m_mouse.get_coords_y(e);
var obj = m_scenes.pick_object(x, y);
...
// 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];
}
}
При перемещении сработает функция main_canvas_move(), которая обеспечит следование объекта за курсором. Для удобства управления объектом мебели будем в этот момент отключать управление камерой:
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);
}
}
}
}
Чтобы определить будущее местоположение объекта, мы рассчитываем координаты точки в пространстве области отрисовки, куда должен проецироваться центр объекта после перемещения. Далее строим трехмерный вектор, направленный из позиции камеры в направлении этой точки, и находим пересечение прямой, содержащей этот вектор, с плоскостью пола комнаты. Точка пересечения и будет искомым местоположением. Далее остается только переместить туда объект.
Рассмотрим подробнее функцию line_plane_intersect():
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);
Она служит для определения точки пересечения прямой и плоскости. Первые два параметра задают плоскость, а именно нормаль (использована константа FLOOR_PLANE_NORMAL) и расстояние от плоскости до центра координат (равно нулю). Эти значения были подобраны в соответствии с моделью комнаты. Также подается специальный объект (_pline_tmp) - прямая в 3-мерном пространстве, которая соответствует лучу, исходящему из точки позиции камеры вдоль направления её взгляда.
Наконец, в методе main_canvas_up() после завершения движения снова включим управление камерой:
function main_canvas_up(e) {
...
if (!_enable_camera_controls) {
m_app.enable_camera_controls();
_enable_camera_controls = true;
}
...
}
Размещение в пределах комнаты
Управление объектами мебели реализовано таким образом, чтобы они двигались параллельно плоскости пола. Чтобы избежать их выхода за пределы комнаты, используем функцию limit_object_position():
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);
}
Поясним кратко происходящее в ней. Во-первых, исходя из модели комнаты известны координаты её стен. В скрипте они заданы в виде констант WALL_X_MAX, WALL_X_MIN, WALL_Z_MAX и WALL_Z_MIN. Во-вторых, для объектов мебели (как и для всех объектов вообще) доступны координаты ограничивающего объема (bounding box). Всё вместе это позволяет отслеживать выход за границу и корректировать местоположение объекта.
Определение пересечений
Для большей наглядности и удобства при расстановке мебели будут отображаться пересечения между объектами. Делать это будем эффектом Outline:
Реализация будет опираться на систему сенсоров.
После динамической загрузки каждой сцены происходит вызов обработчика loaded_cb(), который был передан в качестве параметра в data.load():
function init_controls() {
...
document.getElementById("load-1").addEventListener("click", function(e) {
m_data.load("blend_data/bed.json", loaded_cb, null, null, true);
});
...
}
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);
...
}
...
}
}
Здесь мы получаем вновь загруженные объекты:
var objs = m_scenes.get_all_objects("ALL", data_id);
...затем для каждого проверяем, является ли он физическим:
...
if (m_phy.has_physics(obj)) {
...
}
...
...и для объектов с физикой создаём пару сенсоров:
var sensor_col = m_ctl.create_collision_sensor(obj, "FURNITURE");
var sensor_sel = m_ctl.create_selection_sensor(obj, true);
Метод create_collision_sensor() создаёт сенсор, отслеживающий пересечения с другими физическими объектами. Первым параметром подаётся объект obj, на котором регистрируется сенсор, и который будет проверяться на коллизии, а вторым - свойство Collision ID, выставляемое в физических настройках объектов в Blender'е. Таким образом сенсор будет сообщать о пересечении объекта obj с любым объектом, имеющим данный Collision ID.
В нашем примере все объекты мебели имеют идентификатор FURNITURE и взаимодействуют только между собой.
Метод create_selection_sensor() создаёт сенсор, отслеживающий выделенное состояние объекта. Соответственно, если объект выделен, т.е. выбран, например, нажатием мышью, то сенсор сообщит об этом.
Создадим контейнер для сенсоров - сенсорное множество:
m_ctl.create_sensor_manifold(obj, "COLLISION", m_ctl.CT_CONTINUOUS,
[sensor_col, sensor_sel], logic_func, trigger_outline);
Поясним переданные параметры:
- obj - объект, на котором регистрируется множество сенсоров;
- "COLLISION" - идентификатор сенсорного множества; должен быть уникален для объекта, на котором оно зарегистрировано;
- CT_CONTINUOUS - тип создаваемого множества выбран таким, чтобы функция-обработчик вызывалась постоянно при положительном значения логической функции;
- sensors - массив сенсоров, образующий множество;
- logic_func - логическая функция; её результат совместно с типом множества определяют, когда и как часто будет вызываться функция-обработчик;
- trigger_outline - функция-обработчик.
На вход функции logic_func приходит массив значений всех сенсоров. В нашем примере, функция возвращает положительный результат для выделенного объекта:
function logic_func(s) {
return s[1];
}
Функция trigger_outline довольно проста:
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);
}
}
Здесь используется аргумент pulse (импульс), генерируемый сенсорным множеством. Импульс зависит от результата логической функции и типа сенсорного множества, в частности для типа CT_CONTINUOUS импульс будет положительным (pulse = 1), если логическая функция истинна, и отрицательным (pulse = -1), если она ложна.
В случае положительного импульса, означающего выбранный объект, будем определять наличие пересечений. Для этого проверим значение соответствующего сенсора:
var has_collision = m_ctl.get_sensor_value(obj, id, 0);
Последний параметр здесь указывает индекс сенсора во множестве, которое, напомним, включает в себя 2 сенсора.
Установим красный цвет эффекта Outline (константа OUTLINE_COLOR_ERROR) при наличии пересечения. В противном случае выделенные объекты будут подсвечиваться зеленым цветом (OUTLINE_COLOR_VALID). Таким образом мы наглядно отобразим пересечения объектов мебели между собой.
Заключение
Сегодня мы рассмотрели компоненты приложения, отвечающие за физическую симуляцию и взаимодействие с пользователем. Такие детали очень важны, поскольку добавляют интерактивности, делают игровой процесс более "живым" и захватывающим.
На этом описание программной части приложения "Игровая комната" закончено. Следующая статья цикла будет посвящена созданию моделей приложения в Blender'е.
Исходные файлы приложения и сцены включены в состав бесплатного дистрибутива Blend4Web SDK.
Запустить приложение в отдельном окнеИзменения
[2014-08-22] Изначальная публикация.
[2014-10-29] Обновлен код примера по причине изменения API.
[2014-12-23] Обновлен код примера по причине изменения API.
[2015-04-23] Исправлены некорректные/битые ссылки.
[2015-05-08] Обновлен код примера по причине изменения API.
[2015-05-19] Внесены изменения в код приложения.
[2015-06-26] Внесены изменения в код приложения.
[2015-10-05] Внесены изменения в код приложения.
[2016-08-22] Внесены изменения в код приложения.
[2017-01-12] Исправлены некорректные/битые ссылки.