* `pointed_thing.intersection_normal`: Unit vector, points outwards of the
selected selection box. This specifies which face is pointed at.
Is a null vector `vector.zero()` when the pointer is inside the selection box.
+ For entities with rotated selection boxes, this will be rotated properly
+ by the entity's rotation - it will always be in absolute world space.
* `set_rotation(rot)`
* `rot` is a vector (radians). X is pitch (elevation), Y is yaw (heading)
and Z is roll (bank).
+ * Does not reset rotation incurred through `automatic_rotate`.
+ Remove & readd your objects to force a certain rotation.
* `get_rotation()`: returns the rotation, a vector (radians)
* `set_yaw(yaw)`: sets the yaw in radians (heading).
* `get_yaw()`: returns number in radians
* `liquids`: if false, liquid nodes (`liquidtype ~= "none"`) won't be
returned. Default is false.
+### Limitations
+
+Raycasts don't always work properly for attached objects as the server has no knowledge of models & bones.
+
+**Rotated selectionboxes paired with `automatic_rotate` are not reliable** either since the server
+can't reliably know the total rotation of the objects on different clients (which may differ on a per-client basis).
+The server calculates the total rotation incurred through `automatic_rotate` as a "best guess"
+assuming the object was active & rotating on the client all the time since its creation.
+This may be significantly out of sync with what clients see.
+Additionally, network latency and delayed property sending may create a mismatch of client- & server rotations.
+
+In singleplayer mode, raycasts on objects with rotated selectionboxes & automatic rotate will usually only be slightly off;
+toggling automatic rotation may however cause errors to add up.
+
+In multiplayer mode, the error may be arbitrarily large.
+
### Methods
* `next()`: returns a `pointed_thing` with exact pointing location
collide_with_objects = true,
-- Collide with other objects if physical = true
- collisionbox = {-0.5, -0.5, -0.5, 0.5, 0.5, 0.5},
- selectionbox = {-0.5, -0.5, -0.5, 0.5, 0.5, 0.5},
- -- Selection box uses collision box dimensions when not set.
- -- For both boxes: {xmin, ymin, zmin, xmax, ymax, zmax} in nodes from
- -- object position.
+ collisionbox = { -0.5, -0.5, -0.5, 0.5, 0.5, 0.5 }, -- default
+ selectionbox = { -0.5, -0.5, -0.5, 0.5, 0.5, 0.5, rotate = false },
+ -- { xmin, ymin, zmin, xmax, ymax, zmax } in nodes from object position.
+ -- Collision boxes cannot rotate, setting `rotate = true` on it has no effect.
+ -- If not set, the selection box copies the collision box, and will also not rotate.
+ -- If `rotate = false`, the selection box will not rotate with the object itself, remaining fixed to the axes.
+ -- If `rotate = true`, it will match the object's rotation and any attachment rotations.
+ -- Raycasts use the selection box and object's rotation, but do *not* obey attachment rotations.
+
pointable = true,
-- Whether the object can be pointed at
dofile(minetest.get_modpath("testentities").."/visuals.lua")
+dofile(minetest.get_modpath("testentities").."/selectionbox.lua")
dofile(minetest.get_modpath("testentities").."/armor.lua")
--- /dev/null
+local function color(hex)
+ return ("blank.png^[noalpha^[colorize:#%06X:255"):format(hex)
+end
+
+local function random_color()
+ return color(math.random(0, 0xFFFFFF))
+end
+
+local function random_rotation()
+ return 2 * math.pi * vector.new(math.random(), math.random(), math.random())
+end
+
+local active_selectionbox_entities = 0 -- count active entities
+
+minetest.register_entity("testentities:selectionbox", {
+ initial_properties = {
+ visual = "cube",
+ infotext = "Punch to randomize rotation, rightclick to toggle rotation"
+ },
+ on_activate = function(self)
+ active_selectionbox_entities = active_selectionbox_entities + 1
+
+ local w, h, l = math.random(), math.random(), math.random()
+ self.object:set_properties({
+ textures = {random_color(), random_color(), random_color(), random_color(), random_color(), random_color()},
+ selectionbox = {rotate = true, -w/2, -h/2, -l/2, w/2, h/2, l/2},
+ visual_size = vector.new(w, h, l),
+ automatic_rotate = 2 * math.pi * (math.random() - 0.5)
+ })
+ assert(self.object:get_properties().selectionbox.rotate)
+ self.object:set_armor_groups({punch_operable = 1})
+ self.object:set_rotation(random_rotation())
+ end,
+ on_deactivate = function()
+ active_selectionbox_entities = active_selectionbox_entities - 1
+ end,
+ on_punch = function(self)
+ self.object:set_rotation(random_rotation())
+ end,
+ on_rightclick = function(self)
+ self.object:set_properties({
+ automatic_rotate = self.object:get_properties().automatic_rotate == 0 and 2 * math.pi * (math.random() - 0.5) or 0
+ })
+ end
+})
+
+local hud_ids = {}
+minetest.register_globalstep(function()
+ if active_selectionbox_entities == 0 then
+ return
+ end
+
+ for _, player in pairs(minetest.get_connected_players()) do
+ local offset = player:get_eye_offset()
+ offset.y = offset.y + player:get_properties().eye_height
+ local pos1 = vector.add(player:get_pos(), offset)
+ local raycast = minetest.raycast(pos1, vector.add(pos1, vector.multiply(player:get_look_dir(), 10)), true, false)
+ local pointed_thing = raycast()
+ if pointed_thing.ref == player then
+ pointed_thing = raycast()
+ end
+ local remove_hud_element = true
+ local pname = player:get_player_name()
+ local hud_id = hud_ids[pname]
+ if pointed_thing and pointed_thing.type == "object" then
+ local ent = pointed_thing.ref:get_luaentity()
+ if ent and ent.name == "testentities:selectionbox" then
+ hud_ids[pname] = hud_id or player:hud_add({
+ hud_elem_type = "text", -- See HUD element types
+ position = {x=0.5, y=0.5},
+ text = "X",
+ number = 0xFF0000,
+ alignment = {x=0, y=0},
+ })
+ local shade = math.random(0, 0xFF)
+ minetest.add_particle({
+ -- Random shade of red for the intersection point
+ texture = color(0x10000 * shade),
+ pos = pointed_thing.intersection_point,
+ size = 0.1
+ })
+ minetest.add_particle({
+ -- Same shade of green for the corresponding intersection normal
+ texture = color(0x100 * shade),
+ pos = vector.add(pointed_thing.intersection_point, pointed_thing.intersection_normal * 0.1),
+ size = 0.1
+ })
+ remove_hud_element = false
+ end
+ end
+ if remove_hud_element and hud_id then
+ player:hud_remove(hud_id)
+ hud_ids[pname] = nil
+ end
+ end
+end)
if (!obj->getSelectionBox(&selection_box))
continue;
- const v3f &pos = obj->getPosition();
- aabb3f offsetted_box(selection_box.MinEdge + pos,
- selection_box.MaxEdge + pos);
-
v3f current_intersection;
- v3s16 current_normal;
- if (boxLineCollision(offsetted_box, shootline_on_map.start, line_vector,
- ¤t_intersection, ¤t_normal)) {
- objects.emplace_back((s16) obj->getId(), current_intersection, current_normal,
+ v3f current_normal, current_raw_normal;
+ const v3f rel_pos = shootline_on_map.start - obj->getPosition();
+ bool collision;
+ GenericCAO* gcao = dynamic_cast<GenericCAO*>(obj);
+ if (gcao != nullptr && gcao->getProperties().rotate_selectionbox) {
+ gcao->getSceneNode()->updateAbsolutePosition();
+ const v3f deg = obj->getSceneNode()->getAbsoluteTransformation().getRotationDegrees();
+ collision = boxLineCollision(selection_box, deg,
+ rel_pos, line_vector, ¤t_intersection, ¤t_normal, ¤t_raw_normal);
+ } else {
+ collision = boxLineCollision(selection_box, rel_pos, line_vector,
+ ¤t_intersection, ¤t_normal);
+ current_raw_normal = current_normal;
+ }
+ if (collision) {
+ current_intersection += obj->getPosition();
+ objects.emplace_back(obj->getId(), current_intersection, current_normal, current_raw_normal,
(current_intersection - shootline_on_map.start).getLengthSQ());
}
}
{
std::vector<aabb3f> *selectionboxes = hud->getSelectionBoxes();
selectionboxes->clear();
- hud->setSelectedFaceNormal(v3f(0.0, 0.0, 0.0));
+ hud->setSelectedFaceNormal(v3f());
static thread_local const bool show_entity_selectionbox = g_settings->getBool(
"show_entity_selectionbox");
v3f pos = runData.selected_object->getPosition();
selectionboxes->push_back(aabb3f(selection_box));
hud->setSelectionPos(pos, camera_offset);
+ GenericCAO* gcao = dynamic_cast<GenericCAO*>(runData.selected_object);
+ if (gcao != nullptr && gcao->getProperties().rotate_selectionbox)
+ hud->setSelectionRotation(gcao->getSceneNode()->getAbsoluteTransformation().getRotationDegrees());
+ else
+ hud->setSelectionRotation(v3f());
}
+ hud->setSelectedFaceNormal(result.raw_intersection_normal);
} else if (result.type == POINTEDTHING_NODE) {
// Update selection boxes
MapNode n = map.getNode(result.node_undersurface);
}
hud->setSelectionPos(intToFloat(result.node_undersurface, BS),
camera_offset);
- hud->setSelectedFaceNormal(v3f(
- result.intersection_normal.X,
- result.intersection_normal.Y,
- result.intersection_normal.Z));
+ hud->setSelectionRotation(v3f());
+ hud->setSelectedFaceNormal(result.intersection_normal);
}
// Update selection mesh light level and vertex colors
void Hud::drawSelectionMesh()
{
+ if (m_mode == HIGHLIGHT_NONE || (m_mode == HIGHLIGHT_HALO && !m_selection_mesh))
+ return;
+ const video::SMaterial oldmaterial = driver->getMaterial2D();
+ driver->setMaterial(m_selection_material);
+ const core::matrix4 oldtransform = driver->getTransform(video::ETS_WORLD);
+
+ core::matrix4 translate;
+ translate.setTranslation(m_selection_pos_with_offset);
+ core::matrix4 rotation;
+ rotation.setRotationDegrees(m_selection_rotation);
+ driver->setTransform(video::ETS_WORLD, translate * rotation);
+
if (m_mode == HIGHLIGHT_BOX) {
// Draw 3D selection boxes
- video::SMaterial oldmaterial = driver->getMaterial2D();
- driver->setMaterial(m_selection_material);
for (auto & selection_box : m_selection_boxes) {
- aabb3f box = aabb3f(
- selection_box.MinEdge + m_selection_pos_with_offset,
- selection_box.MaxEdge + m_selection_pos_with_offset);
-
u32 r = (selectionbox_argb.getRed() *
m_selection_mesh_color.getRed() / 255);
u32 g = (selectionbox_argb.getGreen() *
m_selection_mesh_color.getGreen() / 255);
u32 b = (selectionbox_argb.getBlue() *
m_selection_mesh_color.getBlue() / 255);
- driver->draw3DBox(box, video::SColor(255, r, g, b));
+ driver->draw3DBox(selection_box, video::SColor(255, r, g, b));
}
- driver->setMaterial(oldmaterial);
} else if (m_mode == HIGHLIGHT_HALO && m_selection_mesh) {
// Draw selection mesh
- video::SMaterial oldmaterial = driver->getMaterial2D();
- driver->setMaterial(m_selection_material);
setMeshColor(m_selection_mesh, m_selection_mesh_color);
video::SColor face_color(0,
MYMIN(255, m_selection_mesh_color.getRed() * 1.5),
MYMIN(255, m_selection_mesh_color.getBlue() * 1.5));
setMeshColorByNormal(m_selection_mesh, m_selected_face_normal,
face_color);
- scene::IMesh* mesh = cloneMesh(m_selection_mesh);
- translateMesh(mesh, m_selection_pos_with_offset);
u32 mc = m_selection_mesh->getMeshBufferCount();
for (u32 i = 0; i < mc; i++) {
- scene::IMeshBuffer *buf = mesh->getMeshBuffer(i);
+ scene::IMeshBuffer *buf = m_selection_mesh->getMeshBuffer(i);
driver->drawMeshBuffer(buf);
}
- mesh->drop();
- driver->setMaterial(oldmaterial);
}
+ driver->setMaterial(oldmaterial);
+ driver->setTransform(video::ETS_WORLD, oldtransform);
}
enum Hud::BlockBoundsMode Hud::toggleBlockBounds()
v3f getSelectionPos() const { return m_selection_pos; }
+ void setSelectionRotation(v3f rotation) { m_selection_rotation = rotation; }
+
+ v3f getSelectionRotation() const { return m_selection_rotation; }
+
void setSelectionMeshColor(const video::SColor &color)
{
m_selection_mesh_color = color;
std::vector<aabb3f> m_halo_boxes;
v3f m_selection_pos;
v3f m_selection_pos_with_offset;
+ v3f m_selection_rotation;
scene::IMesh *m_selection_mesh = nullptr;
video::SColor m_selection_mesh_color;
// ID of the current box (loop counter)
u16 id = 0;
+ // Do calculations relative to the node center
+ // to translate the ray rather than the boxes
v3f npf = intToFloat(np, BS);
- // This loop translates the boxes to their in-world place.
+ v3f rel_start = state->m_shootline.start - npf;
for (aabb3f &box : boxes) {
- box.MinEdge += npf;
- box.MaxEdge += npf;
-
v3f intersection_point;
- v3s16 intersection_normal;
- if (!boxLineCollision(box, state->m_shootline.start,
+ v3f intersection_normal;
+ if (!boxLineCollision(box, rel_start,
state->m_shootline.getVector(), &intersection_point,
&intersection_normal)) {
++id;
continue;
}
+ intersection_point += npf; // translate back to world coords
f32 distanceSq = (intersection_point
- state->m_shootline.start).getLengthSQ();
// If this is the nearest collision, save it
result.node_real_undersurface = floatToInt(
fake_intersection, BS);
result.node_abovesurface = result.node_real_undersurface
- + result.intersection_normal;
+ + floatToInt(result.intersection_normal, 1.0f);
// Push found PointedThing
state->m_found.push(result);
// If this is nearer than the old nearest object,
os << ", nametag_bgcolor=null ";
os << ", selectionbox=" << PP(selectionbox.MinEdge) << "," << PP(selectionbox.MaxEdge);
+ os << ", rotate_selectionbox=" << rotate_selectionbox;
os << ", pointable=" << pointable;
os << ", static_save=" << static_save;
os << ", eye_height=" << eye_height;
else
writeARGB8(os, nametag_bgcolor.value());
+ writeU8(os, rotate_selectionbox);
// Add stuff only at the bottom.
// Never remove anything, because we don't want new versions of this
}
nametag_bgcolor = bgcolor;
else
nametag_bgcolor = nullopt;
+
+ tmp = readU8(is);
+ if (is.eof())
+ return;
+ rotate_selectionbox = tmp;
} catch (SerializationError &e) {}
}
// Values are BS=1
aabb3f collisionbox = aabb3f(-0.5f, -0.5f, -0.5f, 0.5f, 0.5f, 0.5f);
aabb3f selectionbox = aabb3f(-0.5f, -0.5f, -0.5f, 0.5f, 0.5f, 0.5f);
+ bool rotate_selectionbox = false;
bool pointable = true;
std::string visual = "sprite";
std::string mesh = "";
#include "raycast.h"
#include "irr_v3d.h"
#include "irr_aabb3d.h"
+#include <quaternion.h>
#include "constants.h"
bool RaycastSort::operator() (const PointedThing &pt1,
bool boxLineCollision(const aabb3f &box, const v3f &start,
- const v3f &dir, v3f *collision_point, v3s16 *collision_normal)
+ const v3f &dir, v3f *collision_point, v3f *collision_normal)
{
if (box.isPointInside(start)) {
*collision_point = start;
}
return false;
}
+
+bool boxLineCollision(const aabb3f &box, const v3f &rotation,
+ const v3f &start, const v3f &dir,
+ v3f *collision_point, v3f *collision_normal, v3f *raw_collision_normal)
+{
+ // Inversely transform the ray rather than rotating the box faces;
+ // this allows us to continue using a simple ray - AABB intersection
+ core::quaternion rot(rotation * core::DEGTORAD);
+ rot.makeInverse();
+
+ bool collision = boxLineCollision(box, rot * start, rot * dir, collision_point, collision_normal);
+ if (!collision) return collision;
+
+ // Transform the results back
+ rot.makeInverse();
+ *collision_point = rot * *collision_point;
+ *raw_collision_normal = *collision_normal;
+ *collision_normal = rot * *collision_normal;
+ return collision;
+}
* @returns true if a collision point was found
*/
bool boxLineCollision(const aabb3f &box, const v3f &start, const v3f &dir,
- v3f *collision_point, v3s16 *collision_normal);
+ v3f *collision_point, v3f *collision_normal);
+
+bool boxLineCollision(const aabb3f &box, const v3f &box_rotation,
+ const v3f &start, const v3f &dir,
+ v3f *collision_point, v3f *collision_normal, v3f *raw_collision_normal);
lua_pop(L, 1);
lua_getfield(L, -1, "selectionbox");
- if (lua_istable(L, -1))
+ if (lua_istable(L, -1)) {
+ getboolfield(L, -1, "rotate", prop->rotate_selectionbox);
prop->selectionbox = read_aabb3f(L, -1, 1.0);
- else if (collisionbox_defined)
+ } else if (collisionbox_defined) {
prop->selectionbox = prop->collisionbox;
+ }
lua_pop(L, 1);
getboolfield(L, -1, "pointable", prop->pointable);
push_aabb3f(L, prop->collisionbox);
lua_setfield(L, -2, "collisionbox");
push_aabb3f(L, prop->selectionbox);
+ lua_pushboolean(L, prop->rotate_selectionbox);
+ lua_setfield(L, -2, "rotate");
lua_setfield(L, -2, "selectionbox");
lua_pushboolean(L, prop->pointable);
lua_setfield(L, -2, "pointable");
if (hitpoint && (pointed.type != POINTEDTHING_NOTHING)) {
push_v3f(L, pointed.intersection_point / BS); // convert to node coords
lua_setfield(L, -2, "intersection_point");
- push_v3s16(L, pointed.intersection_normal);
+ push_v3f(L, pointed.intersection_normal);
lua_setfield(L, -2, "intersection_normal");
lua_pushinteger(L, pointed.box_id + 1); // change to Lua array index
lua_setfield(L, -2, "box_id");
}
}
+ if (fabs(m_prop.automatic_rotate) > 0.001f) {
+ m_rotation_add_yaw = modulo360f(m_rotation_add_yaw + dtime * core::RADTODEG *
+ m_prop.automatic_rotate);
+ }
+
if(m_registered) {
m_env->getScriptIface()->luaentity_Step(m_id, dtime, moveresult_p);
}
#include "object_properties.h"
#include "serveractiveobject.h"
+#include <quaternion.h>
+#include "util/numeric.h"
class UnitSAO : public ServerActiveObject
{
// Rotation
void setRotation(v3f rotation) { m_rotation = rotation; }
const v3f &getRotation() const { return m_rotation; }
+ const v3f getTotalRotation() const {
+ // This replicates what happens clientside serverside
+ core::matrix4 rot;
+ setPitchYawRoll(rot, -m_rotation);
+ v3f res;
+ // First rotate by m_rotation, then rotate by the automatic rotate yaw
+ (core::quaternion(v3f(0, -m_rotation_add_yaw * core::DEGTORAD, 0))
+ * core::quaternion(rot.getRotationDegrees() * core::DEGTORAD))
+ .toEuler(res);
+ return res * core::RADTODEG;
+ }
v3f getRadRotation() { return m_rotation * core::DEGTORAD; }
// Deprecated
u16 m_hp = 1;
v3f m_rotation;
+ f32 m_rotation_add_yaw = 0;
ItemGroupList m_armor_groups;
continue;
v3f pos = obj->getBasePosition();
-
- aabb3f offsetted_box(selection_box.MinEdge + pos,
- selection_box.MaxEdge + pos);
+ v3f rel_pos = shootline_on_map.start - pos;
v3f current_intersection;
- v3s16 current_normal;
- if (boxLineCollision(offsetted_box, shootline_on_map.start, line_vector,
- ¤t_intersection, ¤t_normal)) {
+ v3f current_normal;
+ v3f current_raw_normal;
+
+ ObjectProperties *props = obj->accessObjectProperties();
+ bool collision;
+ UnitSAO* usao = dynamic_cast<UnitSAO*>(obj);
+ if (props->rotate_selectionbox && usao != nullptr) {
+ collision = boxLineCollision(selection_box, usao->getTotalRotation(),
+ rel_pos, line_vector, ¤t_intersection, ¤t_normal, ¤t_raw_normal);
+ } else {
+ collision = boxLineCollision(selection_box, rel_pos, line_vector,
+ ¤t_intersection, ¤t_normal);
+ current_raw_normal = current_normal;
+ }
+ if (collision) {
+ current_intersection += pos;
objects.emplace_back(
- (s16) obj->getId(), current_intersection, current_normal,
+ (s16) obj->getId(), current_intersection, current_normal, current_raw_normal,
(current_intersection - shootline_on_map.start).getLengthSQ());
}
}
#include <sstream>
PointedThing::PointedThing(const v3s16 &under, const v3s16 &above,
- const v3s16 &real_under, const v3f &point, const v3s16 &normal,
+ const v3s16 &real_under, const v3f &point, const v3f &normal,
u16 box_id, f32 distSq):
type(POINTEDTHING_NODE),
node_undersurface(under),
distanceSq(distSq)
{}
-PointedThing::PointedThing(u16 id, const v3f &point, const v3s16 &normal,
- f32 distSq) :
+PointedThing::PointedThing(u16 id, const v3f &point,
+ const v3f &normal, const v3f &raw_normal, f32 distSq) :
type(POINTEDTHING_OBJECT),
object_id(id),
intersection_point(point),
intersection_normal(normal),
+ raw_intersection_normal(raw_normal),
distanceSq(distSq)
{}
* This is perpendicular to the face the ray hits,
* points outside of the box and it's length is 1.
*/
- v3s16 intersection_normal;
+ v3f intersection_normal;
+ /*!
+ * Only valid if type is POINTEDTHING_OBJECT.
+ * Raw normal vector of the intersection before applying rotation.
+ */
+ v3f raw_intersection_normal;
/*!
* Only valid if type isn't POINTEDTHING_NONE.
* Indicates which selection box is selected, if there are more of them.
PointedThing() = default;
//! Constructor for POINTEDTHING_NODE
PointedThing(const v3s16 &under, const v3s16 &above,
- const v3s16 &real_under, const v3f &point, const v3s16 &normal,
+ const v3s16 &real_under, const v3f &point, const v3f &normal,
u16 box_id, f32 distSq);
//! Constructor for POINTEDTHING_OBJECT
- PointedThing(u16 id, const v3f &point, const v3s16 &normal, f32 distSq);
+ PointedThing(u16 id, const v3f &point, const v3f &normal, const v3f &raw_normal, f32 distSq);
std::string dump() const;
void serialize(std::ostream &os) const;
void deSerialize(std::istream &is);