Смена материала лазерной указкой
Another way to extend functionality is to add a new action for the object.Еще один вариант расширения функционала, который может пригодиться – это добавление нового действия.
We have a laser pointer that we can grab, throw, and use (turn on a laser beam). Now, let's add an alternative use action - change the pointed object's material. This action will be executed when we press and hold the Grip side button on one controller while using the laser pointer gripped by the opposite controller.Предположим, мы хотим расширить функциональность лазерной указки в нашем шаблоне, которую на данный момент мы можем захватывать, удерживать, бросать и использовать (включать луч), добавив дополнительное действие – циклическая смена материалов по заданному списку на объекте, на который указывает луч (мы уже делали подобное в самом первом ArchViz-проекте). Это действие будет выполняться при использовании (Grip) указки захваченной одним контроллером с зажатой боковой кнопкой использования (Grip) на противоположном контроллере.
The ObjLaserPointer component implements the base functionality of the laser pointer.Базовый функционал лазерной указки реализован в компоненте ObjLaserPointer.
The base component for all interactive objects is VRInteractable (source/Framework/Components/VRInteractable.h). It defines all available states of the interactive objects (grab, hold, use, throw) via the corresponding virtual methods. Components inherited from VRInteractable must implement overrides of these methods. For example, the logic for grabbing the object must be implemented in the grabIt method of the inherited component.Основным компонентом, определяющим базовый функционал всех интерактивных объектов, является VRInteractable (source/Framework/Components/VRInteractable.h). Именно здесь определяются возможные состояния интерактивных объектов (grab, hold, use, throw) через соответствующие виртуальные методы. Компоненты отнаследованные от VRInteractable должны иметь перегрузки этих методов. Например, логику захвата объекта (grab) нужно реализовать в методе grabIt отнаследованного компонента.
So, we need to add a new action and map it to a new state (the Grip side button is pressed on one controller while the laser pointer is gripped (Grip) by the opposite controller is used):Итак, добавим новое действие и свяжем его с новым состоянием (на одном контроллере нажата боковая кнопка Grip, в то время как используется лазерная указка удерживаемая (Grip) вторым контроллером):
-
Add a new useItAlt action (alternative use) to the VRInteractable base component:Добавим новое действие useItAlt (альтернативное использование) в базовый компонент VRInteractable:
Исходный код (C++)#pragma once #include <UnigineNode.h> #include <UniginePhysics.h> #include <UnigineComponentSystem.h> #include "Players/VRPlayer.h" class VRPlayer; class VRInteractable : public Unigine::ComponentBase { public: COMPONENT(VRInteractable, Unigine::ComponentBase); // методы взаимодействия virtual void grabIt(VRPlayer* player, int hand_num) {} virtual void holdIt(VRPlayer* player, int hand_num) {} virtual void useIt(VRPlayer* player, int hand_num) {} virtual void throwIt(VRPlayer* player, int hand_num) {} ${#HL}$virtual void useItAlt(VRPlayer* player, int hand_num) {} ${HL#}$ };
-
Add an override of the useItAlt() method to the ObjLaserPointer component (as we are going to add a new action for the laser pointer). In the update() method, implement the logic of the useItAlt action that is executed when the corresponding flags are set.Добавим перегрузку метода useItAlt() в компонент ObjLaserPointer (поскольку мы хотим добавить новое действие для лазерной указки). Внутри метода update() реализуем логику действия useItAlt, которое будет выполняться когда включены соответствующие флаги.
ПримечаниеDon't forget to change the value in the PROP_AUTOSAVE macro to 1 to enable updating the property created by the Component System as we're going to make changes to the component.Не забудьте изменить значение в макросе PROP_AUTOSAVE на 1 чтобы при перекомпиляции после внесения изменений в код компонента соответствующее свойство (property) обновилось.Исходный код (C++)#pragma once #include <UnigineWorld.h> #include "../../Framework/Components/VRInteractable.h" class ObjLaserPointer : public VRInteractable { public: COMPONENT(ObjLaserPointer, VRInteractable); COMPONENT_INIT(init); COMPONENT_UPDATE(update, 1); // свойство PROP_NAME("laser_pointer"); ${#HL}$PROP_AUTOSAVE(1); ${HL#}$ // параметры PROP_PARAM(Node, laser); PROP_PARAM(Node, laser_ray); PROP_PARAM(Node, laser_hit); ${#HL}$// массив материалов, которые используются при переключении PROP_ARRAY(Material, materials); ${HL#}$ // методы взаимодействия void useIt(VRPlayer* player, int hand_num) override; void grabIt(VRPlayer* player, int hand_num) override; void throwIt(VRPlayer* player, int hand_num) override; ${#HL}$// переопределение для реализации альтернативного действия void useItAlt(VRPlayer* player, int hand_num) override; ${HL#}$ protected: void init(); void update(); private: Unigine::ObjectTextPtr obj_text; Unigine::Math::Mat4 laser_ray_mat; int grabbed; ${#HL}$// флаг, указывающий, что в данный момент выполняется альтернативное действие bool alt_use = false; // индекс текущего материала, который будет применён int current_material = 0; ${HL#}$ Unigine::WorldIntersectionPtr intersection = Unigine::WorldIntersection::create(); };
Исходный код (C++)#include "ObjLaserPointer.h" #include "../../Framework/Utils.h" #include "../Global/ObjectLabeling.h" // мы не хотим получать пересечение луча с самим собой, поэтому используем смещение #define RAY_OFFSET 0.05f REGISTER_COMPONENT(ObjLaserPointer); using namespace Unigine; using namespace Math; void ObjLaserPointer::init() { laser_ray_mat = laser_ray->getTransform(); grabbed = 0; laser->setEnabled(false); } void ObjLaserPointer::update() { if (laser->isEnabled()) { // вычисление позиции попадания лазера laser_ray->setTransform(laser_ray_mat); Vec3 dir = Vec3(laser_ray->getWorldDirection(Math::AXIS_Y)); Vec3 p0 = laser_ray->getWorldPosition() + dir * RAY_OFFSET; Vec3 p1 = p0 + dir * 1000; ObjectPtr hit_obj = World::getIntersection(p0, p1, 1, intersection); if (hit_obj) { laser_ray->setScale(vec3(laser_ray->getScale().x, (float)length(intersection->getPoint() - p0) + RAY_OFFSET, laser_ray->getScale().z)); laser_hit->setWorldPosition(intersection->getPoint()); laser_hit->setEnabled(1); ${#HL}$ // реализация логики действия 'альтернативного использования' (циклическое переключение материалов на объекте из списка) if (alt_use) { // если список материалов пустой/не определён, ничего не делаем if (materials.nullCheck() || materials.size() < 0) return; current_material++; if (current_material >= materials.size()) current_material = 0; hit_obj->setMaterial(materials[current_material], 0); alt_use = false; } ${HL#}$ } else laser_hit->setEnabled(0); // поворот плоскости луча к голове игрока (Y фиксирован, Z свободный) setWorldDirectionY(laser_ray, laser_ray->getWorldDirection(Math::AXIS_Y), vec3(VRPlayer::get()->getHead()->getWorldPosition() - laser_ray->getWorldPosition())); // отображение текста ObjectLabeling *labeling = ObjectLabeling::get(); if (labeling) { if (hit_obj && hit_obj->getProperty() && grabbed) labeling->setTarget(hit_obj, laser_hit->getWorldPosition(), true); } } } void ObjLaserPointer::useIt(VRPlayer* player, int hand_num) { // переключение лазерной указки вкл/выкл (для любого объекта указки) laser->setEnabled(!laser->isEnabled()); } void ObjLaserPointer::grabIt(VRPlayer* player, int hand_num) { grabbed = 1; } void ObjLaserPointer::throwIt(VRPlayer* player, int hand_num) { grabbed = 0; } ${#HL}$// реализация действия, устанавливающего соответствующий флаг void ObjLaserPointer::useItAlt(VRPlayer* player, int hand_num) { alt_use = true; } ${HL#}$
-
Add a new AltUse signal with a callback method and a button to the base VRPlayer class (Framework\Components\Players\VRPlayer.h). All input bindings for VR and PC modes are made in the corresponding classes VRPlayerVR and VRPlayerPC respectively (Framework\Components\Players\).Добавим новый сигнал AltUse с обработчиком и кнопкой в базовый класс VRPlayer (Framework\Components\Players\VRPlayer.h).
Исходный код (C++)#pragma once #include <UnigineNode.h> #include <UnigineNodes.h> #include <UnigineWorld.h> #include <UniginePlayers.h> #include <UniginePhysics.h> #include <UnigineVector.h> #include <UnigineGui.h> #include <UnigineMap.h> #include <UnigineGame.h> #include <UnigineSignal.h> #include <UnigineComponentSystem.h> #include "../VRInteractable.h" class VRInteractable; class VRPlayer : public Unigine::ComponentBase { public: COMPONENT(VRPlayer, Unigine::ComponentBase); COMPONENT_INIT(init); COMPONENT_POST_UPDATE(postUpdate); COMPONENT_SHUTDOWN(shutdown); /////////////////////////////////////// // enums /////////////////////////////////////// enum { HAND_FREE, HAND_GRAB, HAND_HOLD, HAND_THROW }; enum GRAB_MODE { BOUNDBOX, INTERSECTIONS }; enum BUTTONS { TELEPORT = 0, USE, GRAB, MENU, ${#HL}$ ALT_USE ${HL#}$ }; /////////////////////////////////////// // static /////////////////////////////////////// // добавлен ли -extern_plugin "OpenVR/Oculus" в командную строку и успешно ли загружен? static int isVRLoaded(); // возвращает указатель на последний созданный PlayerVR static VRPlayer* get(); /////////////////////////////////////// // методы /////////////////////////////////////// // устанавливает маску пересечений для телепортации (где игрок может находиться) virtual void setRayIntersectionMask(int mask) {} virtual void setTeleportationMask(int mask) {} // игрок virtual Unigine::PlayerPtr getPlayer() { return Unigine::Game::getPlayer(); } virtual void setLock(int lock) {} virtual void setPlayerPosition(const Unigine::Math::Vec3 &pos); // перемещает игрока в указанную позицию (с последующей посадкой на землю) с заданным направлением virtual void landPlayerTo(const Unigine::Math::Vec3 &position, const Unigine::Math::vec3 &direction); UNIGINE_INLINE virtual void landPlayerTo(const Unigine::Math::Mat4 &transform) { landPlayerTo(transform.getColumn3(3), normalize(Unigine::Math::vec3(-transform.getColumn3(2)))); } // голова UNIGINE_INLINE virtual Unigine::NodePtr getHead() { return head; } void setController(bool value) { is_controller = value; } bool isController() { return is_controller; } // руки virtual void setGrabMode(GRAB_MODE mode) { grab_mode = mode; } // захват через пересечение BoundBox-BoundBox или Line-Surface? virtual int getNumHands() { return 0; } // количество рук virtual Unigine::NodePtr getHandNode(int num) = 0; // нода руки virtual int getHandDegreesOfFreedom(int num) = 0; // степени свободы руки (0–5 для ПК, 6 для OpenVR) virtual Unigine::Math::vec3 getHandLinearVelocity(int num) = 0; // линейная скорость руки virtual Unigine::Math::vec3 getHandAngularVelocity(int num) = 0; // угловая скорость руки virtual int getHandState(int num) = 0; // состояние руки (захват, удержание, бросок) virtual const Unigine::NodePtr &getGrabNode(int num) const = 0; // объект, захваченный рукой virtual const Unigine::Vector<VRInteractable*> &getGrabComponents(int num) const = 0; UNIGINE_INLINE virtual Unigine::Math::vec3 getHandyPos() { return Unigine::Math::vec3(0, -0.024f, 0.067f); } UNIGINE_INLINE virtual Unigine::Math::quat getHandyRot() { return Unigine::Math::quat(10.0f, 0, 0); } virtual void visualizeController(int num, bool value) {}; // контроллеры рук virtual int getControllerButtonPressed(int controller_num, VRPlayer::BUTTONS button) = 0; virtual int getControllerButtonDown(int controller_num, VRPlayer::BUTTONS button) = 0; virtual int getControllerButtonUp(int controller_num, VRPlayer::BUTTONS button) = 0; virtual float getControllerAxis(int controller_num, Unigine::InputVRController::AXIS_TYPE axis) = 0; virtual void vibrateController(int controller_num) = 0; // отслеживание взгляда UNIGINE_INLINE virtual bool isEyetrackingValid() const { return false; } UNIGINE_INLINE virtual bool isEyetrackingAvailable() const { return false; } UNIGINE_INLINE virtual Unigine::Math::Vec3 getFocusWorldPosition() const { return Unigine::Math::Vec3_zero; } // колбэки UNIGINE_CALLBACK_METODS(Use, use_signal); UNIGINE_CALLBACK_METODS(Grab, grab_signal); UNIGINE_CALLBACK_METODS(Hold, hold_signal); UNIGINE_CALLBACK_METODS(Throw, throw_signal); ${#HL}$ UNIGINE_CALLBACK_METODS(UseAlt, use_alt_signal); ${HL#}$ protected: void init(); void postUpdate(); void shutdown(); // поиск определённых компонентов на ноде int can_i_grab_it(const Unigine::NodePtr &node); int is_it_switcher(const Unigine::NodePtr &node); int is_it_handle(const Unigine::NodePtr &node); Unigine::NodeDummyPtr head; GRAB_MODE grab_mode = GRAB_MODE::BOUNDBOX; bool is_zero_pose_reseted = false; bool have_grip_button = false; int grip_axis = -1; private: // синглтон static VRPlayer* instance; // сигналы Unigine::Signal use_signal; Unigine::Signal grab_signal; Unigine::Signal hold_signal; Unigine::Signal throw_signal; ${#HL}$ Unigine::Signal use_alt_signal; ${HL#}$ bool is_controller = false; };
Исходный код (C++)#include "VRPlayer.h" #include <UnigineGame.h> #include <UnigineEditor.h> #include <UnigineEngine.h> #include <UnigineWindowManager.h> #include <UnigineVR.h> #include "../Objects/ObjSwitch.h" #include "../Objects/ObjHandle.h" using namespace Unigine; using namespace Math; VRPlayer* VRPlayer::instance; int VRPlayer::isVRLoaded() { return VR::isInitialized() && (VR::getApiType() != VR::API_NULL); } VRPlayer* VRPlayer::get() { return instance; } void VRPlayer::init() { // запуск приложения даже при отсутствии фокуса Engine::get()->setBackgroundUpdate(Engine::BACKGROUND_UPDATE_RENDER_ALWAYS); if(!is_controller) instance = this; is_zero_pose_reseted = VR::resetZeroPose(); } void VRPlayer::postUpdate() { if(!is_zero_pose_reseted) is_zero_pose_reseted = VR::resetZeroPose(); for (int i = 0; i < getNumHands(); i++) { int hand_state = getHandState(i); if (hand_state != HAND_FREE) { auto &components = getGrabComponents(i); switch (hand_state) { case HAND_GRAB: for (int j = 0; j < components.size(); j++) components[j]->grabIt(this, i); grab_signal.invoke(getGrabNode(i)); break; case HAND_HOLD: for (int j = 0; j < components.size(); j++) components[j]->holdIt(this, i); hold_signal.invoke(getGrabNode(i)); break; case HAND_THROW: for (int j = 0; j < components.size(); j++) components[j]->throwIt(this, i); throw_signal.invoke(getGrabNode(i)); break; default: break; } // использование захваченного объекта if (getControllerButtonDown(i, USE)) { for (int j = 0; j < components.size(); j++) components[j]->useIt(this, i); use_signal.invoke(getGrabNode(i)); } ${#HL}$ // альтернативное использование захваченного объекта if (getControllerButtonDown(i, ALT_USE)) { for (int j = 0; j < components.size(); j++) components[j]->useItAlt(this, i); use_alt_signal.invoke(getGrabNode(i)); } ${HL#}$ } } } void VRPlayer::shutdown() { use_signal.clear(); grab_signal.clear(); hold_signal.clear(); throw_signal.clear(); ${#HL}$ use_alt_signal.clear(); ${HL#}$ instance = nullptr; } void VRPlayer::setPlayerPosition(const Vec3 &pos) { getPlayer()->setPosition(pos); } void VRPlayer::landPlayerTo(const Vec3 &position, const vec3 &direction) { Vec3 pos1 = position; Vec3 pos2 = position + Vec3(0, 0, -1) * getPlayer()->getZFar(); WorldIntersectionPtr intersection = WorldIntersection::create(); ObjectPtr hitObj = World::getIntersection(pos1, pos2, 1, intersection); if (hitObj) getPlayer()->setWorldTransform(setTo(intersection->getPoint(), intersection->getPoint() + Vec3(direction), vec3_up)); } int VRPlayer::can_i_grab_it(const NodePtr &node) { return ComponentSystem::get()->getComponent<VRInteractable>(node) != nullptr; } int VRPlayer::is_it_switcher(const NodePtr &node) { return ComponentSystem::get()->getComponent<ObjSwitch>(node) != nullptr; } int VRPlayer::is_it_handle(const NodePtr &node) { return ComponentSystem::get()->getComponent<ObjHandle>(node) != nullptr; }
-
All input bindings for VR and PC modes are made in the corresponding classes VRPlayerVR and VRPlayerPC respectively (Framework\Components\Players\). So we should make some changes to the VRPlayerVR::getControllerButtonDown()method in the VRPlayerVR.cpp file:Все настройки ввода для режимов VR и PC находятся в классах VRPlayerVR и VRPlayerPC соответственно (Framework\Components\Players\). Для VR-режима добавим немного кода в метод VRPlayerVR::getControllerButtonDown() в файле VRPlayerVR.cpp:
Исходный код (C++)// ... int VRPlayerVR::getControllerButtonDown(int controller_num, VRPlayer::BUTTONS button) { if (!controller_valid[controller_num]) return 0; InputVRControllerPtr controller = (controller_num == 0 ? left_controller_device : right_controller_device); ${#HL}$ // получение противоположного контроллера InputVRControllerPtr other_controller = (controller_num == 0 ? right_controller_device : left_controller_device); ${HL#}$ if (!controller) return 0; switch(button) { case VRPlayer::TELEPORT: { int axis_index = controller->findAxisByType(InputVRController::AXIS_TYPE_TRACKPAD_X); if(axis_index == -1) axis_index = controller->findAxisByType(InputVRController::AXIS_TYPE_JOYSTICK_X); if(axis_index != -1) return controller->isButtonDown(Input::VR_BUTTON(Input::VR_BUTTON_AXIS_0 + axis_index)); } case VRPlayer::USE: { int axis_index = controller->findAxisByType(InputVRController::AXIS_TYPE_TRIGGER_VALUE); if(axis_index == -1) axis_index = controller->findAxisByType(InputVRController::AXIS_TYPE_TRIGGER_FORCE); if(axis_index != -1) return controller->getAxis(axis_index) > 0.5f; } case VRPlayer::GRAB: return controller->isButtonDown(Input::VR_BUTTON_GRIP); case VRPlayer::MENU: return controller->isButtonDown(Input::VR_BUTTON_APPLICATION) || controller->isButtonDown(Input::VR_BUTTON_Y); ${#HL}$ case VRPlayer::ALT_USE: { if (!other_controller) return 0; int axis_index = controller->findAxisByType(InputVRController::AXIS_TYPE_TRIGGER_VALUE); if (axis_index == -1) axis_index = controller->findAxisByType(InputVRController::AXIS_TYPE_TRIGGER_FORCE); if (axis_index != -1) return (controller->isButtonPressed(Input::VR_BUTTON_GRIP) && other_controller->isButtonDown(Input::VR_BUTTON_GRIP)); } ${HL#}$ } return 0; } // ...
- Save all the files that we modified and then build and run the application by hitting Ctrl + F5 to make the Component System update properties used to assign the components to nodes. Close the application after running it and switch to UnigineEditor.Сохраните все файлы, которые мы изменили, а затем пересоберите и запустите приложения, нажав Ctrl + F5, чтобы Компонентная система обновила свойства (property), которые используются для связи компонент с объектами. После запуска закройте приложение и переключитесь в UnigineEditor.
-
The only thing left to do is to specify the list of materials that will be applied to the object. Select the laser_pointer node and click Edit in the Parameters window. Then, find the laser_pointer property, specify the number of elements for the materials array (for example, 3), and drag the required materials from the Materials window to fill the array.Единственное, что осталось сделать - это указать список материалов, которые будут назначаться объектам, на которые указывает указка. Выделите ноду laser_pointer и щелкните Edit в окне Parameters. Затем, найдите свойство laser_pointer, задайте число элементов в массиве materials (например, 3), и перетащите в соответствующие поля массива материалы, которые хотите использовать, прямо из окна Materials.
- Save changes (Ctrl + S) and run the application via SDK Browser.Сохраните внесенные изменения (Ctrl + S) и запустите проект через SDK Browser.
Информация, представленная на данной странице, актуальна для версии UNIGINE 2.20 SDK.