/* SPDX-License-Identifier: GPL-2.0-or-later */ /** \file * \ingroup spview3d * * Interactive walk navigation modal operator * (similar to walking around in a first person game). * * \note Similar logic to `view3d_navigate_fly.c` changes here may apply there too. */ /* defines VIEW3D_OT_navigate - walk modal operator */ #include "DNA_object_types.h" #include "DNA_scene_types.h" #include "MEM_guardedalloc.h" #include "BLI_blenlib.h" #include "BLI_kdopbvh.h" #include "BLI_math.h" #include "BLI_utildefines.h" #include "BKE_context.h" #include "BKE_lib_id.h" #include "BKE_main.h" #include "BKE_report.h" #include "BLT_translation.h" #include "WM_api.h" #include "WM_types.h" #include "ED_screen.h" #include "ED_space_api.h" #include "ED_transform_snap_object_context.h" #include "PIL_time.h" /* Smooth-view. */ #include "UI_interface.h" #include "UI_resources.h" #include "GPU_immediate.h" #include "DEG_depsgraph.h" #include "view3d_intern.h" /* own include */ #include "view3d_navigate.h" #ifdef WITH_INPUT_NDOF //# define NDOF_WALK_DEBUG /* is this needed for ndof? - commented so redraw doesn't thrash - campbell */ //# define NDOF_WALK_DRAW_TOOMUCH #endif #define USE_TABLET_SUPPORT /* -------------------------------------------------------------------- */ /** \name Modal Key-map * \{ */ /* NOTE: these defines are saved in keymap files, * do not change values but just add new ones */ enum { WALK_MODAL_CANCEL = 1, WALK_MODAL_CONFIRM, WALK_MODAL_DIR_FORWARD, WALK_MODAL_DIR_FORWARD_STOP, WALK_MODAL_DIR_BACKWARD, WALK_MODAL_DIR_BACKWARD_STOP, WALK_MODAL_DIR_LEFT, WALK_MODAL_DIR_LEFT_STOP, WALK_MODAL_DIR_RIGHT, WALK_MODAL_DIR_RIGHT_STOP, WALK_MODAL_DIR_UP, WALK_MODAL_DIR_UP_STOP, WALK_MODAL_DIR_DOWN, WALK_MODAL_DIR_DOWN_STOP, WALK_MODAL_FAST_ENABLE, WALK_MODAL_FAST_DISABLE, WALK_MODAL_SLOW_ENABLE, WALK_MODAL_SLOW_DISABLE, WALK_MODAL_JUMP, WALK_MODAL_JUMP_STOP, WALK_MODAL_TELEPORT, WALK_MODAL_GRAVITY_TOGGLE, WALK_MODAL_ACCELERATE, WALK_MODAL_DECELERATE, WALK_MODAL_AXIS_LOCK_Z, }; enum { WALK_BIT_FORWARD = 1 << 0, WALK_BIT_BACKWARD = 1 << 1, WALK_BIT_LEFT = 1 << 2, WALK_BIT_RIGHT = 1 << 3, WALK_BIT_UP = 1 << 4, WALK_BIT_DOWN = 1 << 5, }; typedef enum eWalkTeleportState { WALK_TELEPORT_STATE_OFF = 0, WALK_TELEPORT_STATE_ON, } eWalkTeleportState; typedef enum eWalkMethod { WALK_MODE_FREE = 0, WALK_MODE_GRAVITY, } eWalkMethod; typedef enum eWalkGravityState { WALK_GRAVITY_STATE_OFF = 0, WALK_GRAVITY_STATE_JUMP, WALK_GRAVITY_STATE_START, WALK_GRAVITY_STATE_ON, } eWalkGravityState; /* Relative view axis z axis locking. */ typedef enum eWalkLockState { /* Disabled. */ WALK_AXISLOCK_STATE_OFF = 0, /* Moving. */ WALK_AXISLOCK_STATE_ACTIVE = 2, /* Done moving, it cannot be activated again. */ WALK_AXISLOCK_STATE_DONE = 3, } eWalkLockState; void walk_modal_keymap(wmKeyConfig *keyconf) { static const EnumPropertyItem modal_items[] = { {WALK_MODAL_CONFIRM, "CONFIRM", 0, "Confirm", ""}, {WALK_MODAL_CANCEL, "CANCEL", 0, "Cancel", ""}, {WALK_MODAL_DIR_FORWARD, "FORWARD", 0, "Forward", ""}, {WALK_MODAL_DIR_BACKWARD, "BACKWARD", 0, "Backward", ""}, {WALK_MODAL_DIR_LEFT, "LEFT", 0, "Left", ""}, {WALK_MODAL_DIR_RIGHT, "RIGHT", 0, "Right", ""}, {WALK_MODAL_DIR_UP, "UP", 0, "Up", ""}, {WALK_MODAL_DIR_DOWN, "DOWN", 0, "Down", ""}, {WALK_MODAL_DIR_FORWARD_STOP, "FORWARD_STOP", 0, "Stop Move Forward", ""}, {WALK_MODAL_DIR_BACKWARD_STOP, "BACKWARD_STOP", 0, "Stop Mode Backward", ""}, {WALK_MODAL_DIR_LEFT_STOP, "LEFT_STOP", 0, "Stop Move Left", ""}, {WALK_MODAL_DIR_RIGHT_STOP, "RIGHT_STOP", 0, "Stop Mode Right", ""}, {WALK_MODAL_DIR_UP_STOP, "UP_STOP", 0, "Stop Move Up", ""}, {WALK_MODAL_DIR_DOWN_STOP, "DOWN_STOP", 0, "Stop Mode Down", ""}, {WALK_MODAL_TELEPORT, "TELEPORT", 0, "Teleport", "Move forward a few units at once"}, {WALK_MODAL_ACCELERATE, "ACCELERATE", 0, "Accelerate", ""}, {WALK_MODAL_DECELERATE, "DECELERATE", 0, "Decelerate", ""}, {WALK_MODAL_FAST_ENABLE, "FAST_ENABLE", 0, "Fast", "Move faster (walk or fly)"}, {WALK_MODAL_FAST_DISABLE, "FAST_DISABLE", 0, "Fast (Off)", "Resume regular speed"}, {WALK_MODAL_SLOW_ENABLE, "SLOW_ENABLE", 0, "Slow", "Move slower (walk or fly)"}, {WALK_MODAL_SLOW_DISABLE, "SLOW_DISABLE", 0, "Slow (Off)", "Resume regular speed"}, {WALK_MODAL_JUMP, "JUMP", 0, "Jump", "Jump when in walk mode"}, {WALK_MODAL_JUMP_STOP, "JUMP_STOP", 0, "Jump (Off)", "Stop pushing jump"}, {WALK_MODAL_GRAVITY_TOGGLE, "GRAVITY_TOGGLE", 0, "Toggle Gravity", "Toggle gravity effect"}, {WALK_MODAL_AXIS_LOCK_Z, "AXIS_LOCK_Z", 0, "Z Axis Correction", "Z axis correction"}, {0, NULL, 0, NULL, NULL}, }; wmKeyMap *keymap = WM_modalkeymap_find(keyconf, "View3D Walk Modal"); /* This function is called for each space-type, only needs to add map once. */ if (keymap && keymap->modal_items) { return; } keymap = WM_modalkeymap_ensure(keyconf, "View3D Walk Modal", modal_items); /* assign map to operators */ WM_modalkeymap_assign(keymap, "VIEW3D_OT_walk"); } /** \} */ /* -------------------------------------------------------------------- */ /** \name Internal Walk Structs * \{ */ typedef struct WalkTeleport { eWalkTeleportState state; float duration; /* from user preferences */ float origin[3]; float direction[3]; double initial_time; eWalkMethod navigation_mode; /* teleport always set FREE mode on */ } WalkTeleport; typedef struct WalkInfo { /* context stuff */ RegionView3D *rv3d; View3D *v3d; ARegion *region; struct Depsgraph *depsgraph; Scene *scene; /** Needed for updating that isn't triggered by input. */ wmTimer *timer; short state; bool redraw; /** * Needed for auto-keyframing, when animation isn't playing, only keyframe on confirmation. * * Currently we can't cancel this operator usefully while recording on animation playback * (this would need to un-key all previous frames). */ bool anim_playing; bool need_rotation_keyframe; bool need_translation_keyframe; /** Previous 2D mouse values. */ int prev_mval[2]; /** Initial mouse location. */ int init_mval[2]; int moffset[2]; #ifdef WITH_INPUT_NDOF /** Latest 3D mouse values. */ wmNDOFMotionData *ndof; #endif /* Walk state. */ /** The base speed without run/slow down modifications. */ float base_speed; /** The speed the view is moving per redraw. */ float speed; /** World scale 1.0 default. */ float grid; /* compare between last state */ /** Time between draws. */ double time_lastdraw; void *draw_handle_pixel; /* use for some lag */ /** Keep the previous value to smooth transitions (use lag). */ float dvec_prev[3]; /** Walk/free movement. */ eWalkMethod navigation_mode; /* teleport */ WalkTeleport teleport; /** Look speed factor - user preferences. */ float mouse_speed; /** Speed adjustments. */ bool is_fast; bool is_slow; /** Mouse reverse. */ bool is_reversed; #ifdef USE_TABLET_SUPPORT /** Tablet devices (we can't relocate the cursor). */ bool is_cursor_absolute; #endif /** Gravity system. */ eWalkGravityState gravity_state; float gravity; /** Height to use in walk mode. */ float view_height; /** Counting system to allow movement to continue if a direction (WASD) key is still pressed. */ int active_directions; float speed_jump; /** Maximum jump height. */ float jump_height; /** To use for fast/slow speeds. */ float speed_factor; eWalkLockState zlock; /** Nicer dynamics. */ float zlock_momentum; struct SnapObjectContext *snap_context; struct View3DCameraControl *v3d_camera_control; } WalkInfo; /** \} */ /* -------------------------------------------------------------------- */ /** \name Internal Walk Drawing * \{ */ /* prototypes */ #ifdef WITH_INPUT_NDOF static void walkApply_ndof(bContext *C, WalkInfo *walk, bool is_confirm); #endif /* WITH_INPUT_NDOF */ static int walkApply(bContext *C, struct WalkInfo *walk, bool is_confirm); static float getVelocityZeroTime(const float gravity, const float velocity); static void drawWalkPixel(const struct bContext *UNUSED(C), ARegion *region, void *arg) { /* draws an aim/cross in the center */ WalkInfo *walk = arg; const int outter_length = 24; const int inner_length = 14; int xoff, yoff; rctf viewborder; if (ED_view3d_cameracontrol_object_get(walk->v3d_camera_control)) { ED_view3d_calc_camera_border( walk->scene, walk->depsgraph, region, walk->v3d, walk->rv3d, &viewborder, false); xoff = viewborder.xmin + BLI_rctf_size_x(&viewborder) * 0.5f; yoff = viewborder.ymin + BLI_rctf_size_y(&viewborder) * 0.5f; } else { xoff = walk->region->winx / 2; yoff = walk->region->winy / 2; } GPUVertFormat *format = immVertexFormat(); uint pos = GPU_vertformat_attr_add(format, "pos", GPU_COMP_I32, 2, GPU_FETCH_INT_TO_FLOAT); immBindBuiltinProgram(GPU_SHADER_3D_UNIFORM_COLOR); immUniformThemeColorAlpha(TH_VIEW_OVERLAY, 1.0f); immBegin(GPU_PRIM_LINES, 8); /* North */ immVertex2i(pos, xoff, yoff + inner_length); immVertex2i(pos, xoff, yoff + outter_length); /* East */ immVertex2i(pos, xoff + inner_length, yoff); immVertex2i(pos, xoff + outter_length, yoff); /* South */ immVertex2i(pos, xoff, yoff - inner_length); immVertex2i(pos, xoff, yoff - outter_length); /* West */ immVertex2i(pos, xoff - inner_length, yoff); immVertex2i(pos, xoff - outter_length, yoff); immEnd(); immUnbindProgram(); } /** \} */ /* -------------------------------------------------------------------- */ /** \name Internal Walk Logic * \{ */ static void walk_navigation_mode_set(WalkInfo *walk, eWalkMethod mode) { if (mode == WALK_MODE_FREE) { walk->navigation_mode = WALK_MODE_FREE; walk->gravity_state = WALK_GRAVITY_STATE_OFF; } else { /* WALK_MODE_GRAVITY */ walk->navigation_mode = WALK_MODE_GRAVITY; walk->gravity_state = WALK_GRAVITY_STATE_START; } } /** * \param r_distance: Distance to the hit point */ static bool walk_floor_distance_get(RegionView3D *rv3d, WalkInfo *walk, const float dvec[3], float *r_distance) { const float ray_normal[3] = {0, 0, -1}; /* down */ float ray_start[3]; float r_location[3]; float r_normal_dummy[3]; float dvec_tmp[3]; bool ret; *r_distance = BVH_RAYCAST_DIST_MAX; copy_v3_v3(ray_start, rv3d->viewinv[3]); mul_v3_v3fl(dvec_tmp, dvec, walk->grid); add_v3_v3(ray_start, dvec_tmp); ret = ED_transform_snap_object_project_ray( walk->snap_context, walk->depsgraph, walk->v3d, &(const struct SnapObjectParams){ .snap_target_select = SCE_SNAP_TARGET_ALL, /* Avoid having to convert the edit-mesh to a regular mesh. */ .edit_mode_type = SNAP_GEOM_EDIT, }, ray_start, ray_normal, r_distance, r_location, r_normal_dummy); /* artificially scale the distance to the scene size */ *r_distance /= walk->grid; return ret; } /** * \param ray_distance: Distance to the hit point * \param r_location: Location of the hit point * \param r_normal: Normal of the hit surface, transformed to always face the camera */ static bool walk_ray_cast(RegionView3D *rv3d, WalkInfo *walk, float r_location[3], float r_normal[3], float *ray_distance) { float ray_normal[3] = {0, 0, -1}; /* forward */ float ray_start[3]; bool ret; *ray_distance = BVH_RAYCAST_DIST_MAX; copy_v3_v3(ray_start, rv3d->viewinv[3]); mul_mat3_m4_v3(rv3d->viewinv, ray_normal); normalize_v3(ray_normal); ret = ED_transform_snap_object_project_ray(walk->snap_context, walk->depsgraph, walk->v3d, &(const struct SnapObjectParams){ .snap_target_select = SCE_SNAP_TARGET_ALL, }, ray_start, ray_normal, NULL, r_location, r_normal); /* dot is positive if both rays are facing the same direction */ if (dot_v3v3(ray_normal, r_normal) > 0) { negate_v3(r_normal); } /* artificially scale the distance to the scene size */ *ray_distance /= walk->grid; return ret; } /* WalkInfo->state */ enum { WALK_RUNNING = 0, WALK_CANCEL = 1, WALK_CONFIRM = 2, }; /* keep the previous speed until user changes userpreferences */ static float base_speed = -1.0f; static float userdef_speed = -1.0f; static bool initWalkInfo(bContext *C, WalkInfo *walk, wmOperator *op, const int mval[2]) { wmWindowManager *wm = CTX_wm_manager(C); wmWindow *win = CTX_wm_window(C); walk->rv3d = CTX_wm_region_view3d(C); walk->v3d = CTX_wm_view3d(C); walk->region = CTX_wm_region(C); walk->depsgraph = CTX_data_ensure_evaluated_depsgraph(C); walk->scene = CTX_data_scene(C); #ifdef NDOF_WALK_DEBUG puts("\n-- walk begin --"); #endif /* sanity check: for rare but possible case (if lib-linking the camera fails) */ if ((walk->rv3d->persp == RV3D_CAMOB) && (walk->v3d->camera == NULL)) { walk->rv3d->persp = RV3D_PERSP; } if (walk->rv3d->persp == RV3D_CAMOB && !BKE_id_is_editable(CTX_data_main(C), &walk->v3d->camera->id)) { BKE_report(op->reports, RPT_ERROR, "Cannot navigate a camera from an external library or non-editable override"); return false; } if (ED_view3d_offset_lock_check(walk->v3d, walk->rv3d)) { BKE_report(op->reports, RPT_ERROR, "Cannot navigate when the view offset is locked"); return false; } if (walk->rv3d->persp == RV3D_CAMOB && walk->v3d->camera->constraints.first) { BKE_report(op->reports, RPT_ERROR, "Cannot navigate an object with constraints"); return false; } walk->state = WALK_RUNNING; if (fabsf(U.walk_navigation.walk_speed - userdef_speed) > 0.1f) { base_speed = U.walk_navigation.walk_speed; userdef_speed = U.walk_navigation.walk_speed; } walk->speed = 0.0f; walk->is_fast = false; walk->is_slow = false; walk->grid = (walk->scene->unit.system == USER_UNIT_NONE) ? 1.0f : 1.0f / walk->scene->unit.scale_length; /* user preference settings */ walk->teleport.duration = U.walk_navigation.teleport_time; walk->mouse_speed = U.walk_navigation.mouse_speed; if (U.walk_navigation.flag & USER_WALK_GRAVITY) { walk_navigation_mode_set(walk, WALK_MODE_GRAVITY); } else { walk_navigation_mode_set(walk, WALK_MODE_FREE); } walk->view_height = U.walk_navigation.view_height; walk->jump_height = U.walk_navigation.jump_height; walk->speed = U.walk_navigation.walk_speed; walk->speed_factor = U.walk_navigation.walk_speed_factor; walk->zlock = WALK_AXISLOCK_STATE_OFF; walk->gravity_state = WALK_GRAVITY_STATE_OFF; if (walk->scene->physics_settings.flag & PHYS_GLOBAL_GRAVITY) { walk->gravity = fabsf(walk->scene->physics_settings.gravity[2]); } else { walk->gravity = 9.80668f; /* m/s2 */ } walk->is_reversed = ((U.walk_navigation.flag & USER_WALK_MOUSE_REVERSE) != 0); #ifdef USE_TABLET_SUPPORT walk->is_cursor_absolute = false; #endif walk->active_directions = 0; #ifdef NDOF_WALK_DRAW_TOOMUCH walk->redraw = 1; #endif zero_v3(walk->dvec_prev); walk->timer = WM_event_add_timer(CTX_wm_manager(C), win, TIMER, 0.01f); #ifdef WITH_INPUT_NDOF walk->ndof = NULL; #endif walk->anim_playing = ED_screen_animation_playing(wm); walk->need_rotation_keyframe = false; walk->need_translation_keyframe = false; walk->time_lastdraw = PIL_check_seconds_timer(); walk->draw_handle_pixel = ED_region_draw_cb_activate( walk->region->type, drawWalkPixel, walk, REGION_DRAW_POST_PIXEL); walk->rv3d->rflag |= RV3D_NAVIGATING; walk->snap_context = ED_transform_snap_object_context_create(walk->scene, 0); walk->v3d_camera_control = ED_view3d_cameracontrol_acquire( walk->depsgraph, walk->scene, walk->v3d, walk->rv3d); copy_v2_v2_int(walk->init_mval, mval); copy_v2_v2_int(walk->prev_mval, mval); WM_cursor_grab_enable(win, 0, true, NULL); return 1; } static int walkEnd(bContext *C, WalkInfo *walk) { wmWindow *win; RegionView3D *rv3d; if (walk->state == WALK_RUNNING) { return OPERATOR_RUNNING_MODAL; } if (walk->state == WALK_CONFIRM) { /* Needed for auto_keyframe. */ #ifdef WITH_INPUT_NDOF if (walk->ndof) { walkApply_ndof(C, walk, true); } else #endif /* WITH_INPUT_NDOF */ { walkApply(C, walk, true); } } #ifdef NDOF_WALK_DEBUG puts("\n-- walk end --"); #endif win = CTX_wm_window(C); rv3d = walk->rv3d; WM_event_remove_timer(CTX_wm_manager(C), win, walk->timer); ED_region_draw_cb_exit(walk->region->type, walk->draw_handle_pixel); ED_transform_snap_object_context_destroy(walk->snap_context); ED_view3d_cameracontrol_release(walk->v3d_camera_control, walk->state == WALK_CANCEL); rv3d->rflag &= ~RV3D_NAVIGATING; #ifdef WITH_INPUT_NDOF if (walk->ndof) { MEM_freeN(walk->ndof); } #endif WM_cursor_grab_enable(win, 0, true, NULL); if (walk->state == WALK_CONFIRM) { MEM_freeN(walk); return OPERATOR_FINISHED; } MEM_freeN(walk); return OPERATOR_CANCELLED; } static void walkEvent(WalkInfo *walk, const wmEvent *event) { if (event->type == TIMER && event->customdata == walk->timer) { walk->redraw = true; } else if (ISMOUSE_MOTION(event->type)) { #ifdef USE_TABLET_SUPPORT if ((walk->is_cursor_absolute == false) && event->tablet.is_motion_absolute) { walk->is_cursor_absolute = true; } #endif /* USE_TABLET_SUPPORT */ walk->moffset[0] += event->mval[0] - walk->prev_mval[0]; walk->moffset[1] += event->mval[1] - walk->prev_mval[1]; copy_v2_v2_int(walk->prev_mval, event->mval); if (walk->moffset[0] || walk->moffset[1]) { walk->redraw = true; } } #ifdef WITH_INPUT_NDOF else if (event->type == NDOF_MOTION) { /* do these automagically get delivered? yes. */ // puts("ndof motion detected in walk mode!"); // static const char *tag_name = "3D mouse position"; const wmNDOFMotionData *incoming_ndof = event->customdata; switch (incoming_ndof->progress) { case P_STARTING: /* start keeping track of 3D mouse position */ # ifdef NDOF_WALK_DEBUG puts("start keeping track of 3D mouse position"); # endif /* fall-through */ case P_IN_PROGRESS: /* update 3D mouse position */ # ifdef NDOF_WALK_DEBUG putchar('.'); fflush(stdout); # endif if (walk->ndof == NULL) { // walk->ndof = MEM_mallocN(sizeof(wmNDOFMotionData), tag_name); walk->ndof = MEM_dupallocN(incoming_ndof); // walk->ndof = malloc(sizeof(wmNDOFMotionData)); } else { memcpy(walk->ndof, incoming_ndof, sizeof(wmNDOFMotionData)); } break; case P_FINISHING: /* stop keeping track of 3D mouse position */ # ifdef NDOF_WALK_DEBUG puts("stop keeping track of 3D mouse position"); # endif if (walk->ndof) { MEM_freeN(walk->ndof); // free(walk->ndof); walk->ndof = NULL; } /* update the time else the view will jump when 2D mouse/timer resume */ walk->time_lastdraw = PIL_check_seconds_timer(); break; default: break; /* should always be one of the above 3 */ } } #endif /* WITH_INPUT_NDOF */ /* handle modal keymap first */ else if (event->type == EVT_MODAL_MAP) { switch (event->val) { case WALK_MODAL_CANCEL: walk->state = WALK_CANCEL; break; case WALK_MODAL_CONFIRM: walk->state = WALK_CONFIRM; break; case WALK_MODAL_ACCELERATE: base_speed *= 1.0f + (walk->is_slow ? 0.01f : 0.1f); break; case WALK_MODAL_DECELERATE: base_speed /= 1.0f + (walk->is_slow ? 0.01f : 0.1f); break; /* implement WASD keys */ case WALK_MODAL_DIR_FORWARD: walk->active_directions |= WALK_BIT_FORWARD; break; case WALK_MODAL_DIR_BACKWARD: walk->active_directions |= WALK_BIT_BACKWARD; break; case WALK_MODAL_DIR_LEFT: walk->active_directions |= WALK_BIT_LEFT; break; case WALK_MODAL_DIR_RIGHT: walk->active_directions |= WALK_BIT_RIGHT; break; case WALK_MODAL_DIR_UP: walk->active_directions |= WALK_BIT_UP; break; case WALK_MODAL_DIR_DOWN: walk->active_directions |= WALK_BIT_DOWN; break; case WALK_MODAL_DIR_FORWARD_STOP: walk->active_directions &= ~WALK_BIT_FORWARD; break; case WALK_MODAL_DIR_BACKWARD_STOP: walk->active_directions &= ~WALK_BIT_BACKWARD; break; case WALK_MODAL_DIR_LEFT_STOP: walk->active_directions &= ~WALK_BIT_LEFT; break; case WALK_MODAL_DIR_RIGHT_STOP: walk->active_directions &= ~WALK_BIT_RIGHT; break; case WALK_MODAL_DIR_UP_STOP: walk->active_directions &= ~WALK_BIT_UP; break; case WALK_MODAL_DIR_DOWN_STOP: walk->active_directions &= ~WALK_BIT_DOWN; break; case WALK_MODAL_FAST_ENABLE: walk->is_fast = true; break; case WALK_MODAL_FAST_DISABLE: walk->is_fast = false; break; case WALK_MODAL_SLOW_ENABLE: walk->is_slow = true; break; case WALK_MODAL_SLOW_DISABLE: walk->is_slow = false; break; #define JUMP_SPEED_MIN 1.0f #define JUMP_TIME_MAX 0.2f /* s */ #define JUMP_SPEED_MAX sqrtf(2.0f * walk->gravity * walk->jump_height) case WALK_MODAL_JUMP_STOP: if (walk->gravity_state == WALK_GRAVITY_STATE_JUMP) { float t; /* delta time */ t = (float)(PIL_check_seconds_timer() - walk->teleport.initial_time); /* Reduce the velocity, if JUMP wasn't hold for long enough. */ t = min_ff(t, JUMP_TIME_MAX); walk->speed_jump = JUMP_SPEED_MIN + t * (JUMP_SPEED_MAX - JUMP_SPEED_MIN) / JUMP_TIME_MAX; /* when jumping, duration is how long it takes before we start going down */ walk->teleport.duration = getVelocityZeroTime(walk->gravity, walk->speed_jump); /* no more increase of jump speed */ walk->gravity_state = WALK_GRAVITY_STATE_ON; } break; case WALK_MODAL_JUMP: if ((walk->navigation_mode == WALK_MODE_GRAVITY) && (walk->gravity_state == WALK_GRAVITY_STATE_OFF) && (walk->teleport.state == WALK_TELEPORT_STATE_OFF)) { /* no need to check for ground, * walk->gravity wouldn't be off * if we were over a hole */ walk->gravity_state = WALK_GRAVITY_STATE_JUMP; walk->speed_jump = JUMP_SPEED_MAX; walk->teleport.initial_time = PIL_check_seconds_timer(); copy_v3_v3(walk->teleport.origin, walk->rv3d->viewinv[3]); /* using previous vec because WASD keys are not called when SPACE is */ copy_v2_v2(walk->teleport.direction, walk->dvec_prev); /* when jumping, duration is how long it takes before we start going down */ walk->teleport.duration = getVelocityZeroTime(walk->gravity, walk->speed_jump); } break; case WALK_MODAL_TELEPORT: { float loc[3], nor[3]; float distance; bool ret = walk_ray_cast(walk->rv3d, walk, loc, nor, &distance); /* in case we are teleporting middle way from a jump */ walk->speed_jump = 0.0f; if (ret) { WalkTeleport *teleport = &walk->teleport; /* Store the current navigation mode if we are not already teleporting. */ if (teleport->state == WALK_TELEPORT_STATE_OFF) { teleport->navigation_mode = walk->navigation_mode; } teleport->state = WALK_TELEPORT_STATE_ON; teleport->initial_time = PIL_check_seconds_timer(); teleport->duration = U.walk_navigation.teleport_time; walk_navigation_mode_set(walk, WALK_MODE_FREE); copy_v3_v3(teleport->origin, walk->rv3d->viewinv[3]); /* stop the camera from a distance (camera height) */ normalize_v3_length(nor, walk->view_height); add_v3_v3(loc, nor); sub_v3_v3v3(teleport->direction, loc, teleport->origin); } break; } #undef JUMP_SPEED_MAX #undef JUMP_TIME_MAX #undef JUMP_SPEED_MIN case WALK_MODAL_GRAVITY_TOGGLE: if (walk->navigation_mode == WALK_MODE_GRAVITY) { walk_navigation_mode_set(walk, WALK_MODE_FREE); } else { /* WALK_MODE_FREE */ walk_navigation_mode_set(walk, WALK_MODE_GRAVITY); } break; case WALK_MODAL_AXIS_LOCK_Z: if (walk->zlock != WALK_AXISLOCK_STATE_DONE) { walk->zlock = WALK_AXISLOCK_STATE_ACTIVE; walk->zlock_momentum = 0.0f; } break; } } } static void walkMoveCamera(bContext *C, WalkInfo *walk, const bool do_rotate, const bool do_translate, const bool is_confirm) { /* we only consider autokeying on playback or if user confirmed walk on the same frame * otherwise we get a keyframe even if the user cancels. */ const bool use_autokey = is_confirm || walk->anim_playing; ED_view3d_cameracontrol_update( walk->v3d_camera_control, use_autokey, C, do_rotate, do_translate); if (use_autokey) { walk->need_rotation_keyframe = false; walk->need_translation_keyframe = false; } } static float getFreeFallDistance(const float gravity, const float time) { return gravity * (time * time) * 0.5f; } static float getVelocityZeroTime(const float gravity, const float velocity) { return velocity / gravity; } static int walkApply(bContext *C, WalkInfo *walk, bool is_confirm) { #define WALK_ROTATE_TABLET_FAC 8.8f /* Higher is faster, relative to region size. */ #define WALK_ROTATE_CONSTANT_FAC DEG2RAD(0.15f) /* Higher is faster, radians per-pixel. */ #define WALK_TOP_LIMIT DEG2RADF(85.0f) #define WALK_BOTTOM_LIMIT DEG2RADF(-80.0f) #define WALK_MOVE_SPEED base_speed #define WALK_BOOST_FACTOR ((void)0, walk->speed_factor) #define WALK_ZUP_CORRECT_FAC 0.1f /* Amount to correct per step. */ #define WALK_ZUP_CORRECT_ACCEL 0.05f /* Increase upright momentum each step. */ RegionView3D *rv3d = walk->rv3d; ARegion *region = walk->region; /* 3x3 copy of the view matrix so we can move along the view axis */ float mat[3][3]; /* this is the direction that's added to the view offset per redraw */ float dvec[3] = {0.0f, 0.0f, 0.0f}; int moffset[2]; /* mouse offset from the views center */ float tmp_quat[4]; /* used for rotating the view */ #ifdef NDOF_WALK_DEBUG { static uint iteration = 1; printf("walk timer %d\n", iteration++); } #endif { /* mouse offset from the center */ copy_v2_v2_int(moffset, walk->moffset); /* apply moffset so we can re-accumulate */ walk->moffset[0] = 0; walk->moffset[1] = 0; /* revert mouse */ if (walk->is_reversed) { moffset[1] = -moffset[1]; } /* Should we redraw? */ if ((walk->active_directions) || moffset[0] || moffset[1] || walk->zlock == WALK_AXISLOCK_STATE_ACTIVE || walk->gravity_state != WALK_GRAVITY_STATE_OFF || walk->teleport.state == WALK_TELEPORT_STATE_ON || is_confirm) { float dvec_tmp[3]; /* time how fast it takes for us to redraw, * this is so simple scenes don't walk too fast */ double time_current; float time_redraw; float time_redraw_clamped; #ifdef NDOF_WALK_DRAW_TOOMUCH walk->redraw = 1; #endif time_current = PIL_check_seconds_timer(); time_redraw = (float)(time_current - walk->time_lastdraw); /* Clamp redraw time to avoid jitter in roll correction. */ time_redraw_clamped = min_ff(0.05f, time_redraw); walk->time_lastdraw = time_current; /* base speed in m/s */ walk->speed = WALK_MOVE_SPEED; if (walk->is_fast) { walk->speed *= WALK_BOOST_FACTOR; } else if (walk->is_slow) { walk->speed *= 1.0f / WALK_BOOST_FACTOR; } copy_m3_m4(mat, rv3d->viewinv); { /* rotate about the X axis- look up/down */ if (moffset[1]) { float upvec[3]; float angle; float y; /* relative offset */ y = (float)moffset[1]; /* Speed factor. */ #ifdef USE_TABLET_SUPPORT if (walk->is_cursor_absolute) { y /= region->winy; y *= WALK_ROTATE_TABLET_FAC; } else #endif { y *= WALK_ROTATE_CONSTANT_FAC; } /* user adjustment factor */ y *= walk->mouse_speed; /* clamp the angle limits */ /* it ranges from 90.0f to -90.0f */ angle = -asinf(rv3d->viewmat[2][2]); if (angle > WALK_TOP_LIMIT && y > 0.0f) { y = 0.0f; } else if (angle < WALK_BOTTOM_LIMIT && y < 0.0f) { y = 0.0f; } copy_v3_fl3(upvec, 1.0f, 0.0f, 0.0f); mul_m3_v3(mat, upvec); /* Rotate about the relative up vec */ axis_angle_to_quat(tmp_quat, upvec, -y); mul_qt_qtqt(rv3d->viewquat, rv3d->viewquat, tmp_quat); } /* rotate about the Y axis- look left/right */ if (moffset[0]) { float upvec[3]; float x; /* if we're upside down invert the moffset */ copy_v3_fl3(upvec, 0.0f, 1.0f, 0.0f); mul_m3_v3(mat, upvec); if (upvec[2] < 0.0f) { moffset[0] = -moffset[0]; } /* relative offset */ x = (float)moffset[0]; /* Speed factor. */ #ifdef USE_TABLET_SUPPORT if (walk->is_cursor_absolute) { x /= region->winx; x *= WALK_ROTATE_TABLET_FAC; } else #endif { x *= WALK_ROTATE_CONSTANT_FAC; } /* user adjustment factor */ x *= walk->mouse_speed; /* Rotate about the relative up vec */ axis_angle_to_quat_single(tmp_quat, 'Z', x); mul_qt_qtqt(rv3d->viewquat, rv3d->viewquat, tmp_quat); } if (walk->zlock == WALK_AXISLOCK_STATE_ACTIVE) { float upvec[3]; copy_v3_fl3(upvec, 1.0f, 0.0f, 0.0f); mul_m3_v3(mat, upvec); /* Make sure we have some z rolling. */ if (fabsf(upvec[2]) > 0.00001f) { float roll = upvec[2] * 5.0f; /* Rotate the view about this axis. */ copy_v3_fl3(upvec, 0.0f, 0.0f, 1.0f); mul_m3_v3(mat, upvec); /* Rotate about the relative up vec. */ axis_angle_to_quat(tmp_quat, upvec, roll * time_redraw_clamped * walk->zlock_momentum * WALK_ZUP_CORRECT_FAC); mul_qt_qtqt(rv3d->viewquat, rv3d->viewquat, tmp_quat); walk->zlock_momentum += WALK_ZUP_CORRECT_ACCEL; } else { /* Lock fixed, don't need to check it ever again. */ walk->zlock = WALK_AXISLOCK_STATE_DONE; } } } /* WASD - 'move' translation code */ if ((walk->active_directions) && (walk->gravity_state == WALK_GRAVITY_STATE_OFF)) { short direction; zero_v3(dvec); if ((walk->active_directions & WALK_BIT_FORWARD) || (walk->active_directions & WALK_BIT_BACKWARD)) { direction = 0; if (walk->active_directions & WALK_BIT_FORWARD) { direction += 1; } if (walk->active_directions & WALK_BIT_BACKWARD) { direction -= 1; } copy_v3_fl3(dvec_tmp, 0.0f, 0.0f, direction); mul_m3_v3(mat, dvec_tmp); if (walk->navigation_mode == WALK_MODE_GRAVITY) { dvec_tmp[2] = 0.0f; } add_v3_v3(dvec, dvec_tmp); } if ((walk->active_directions & WALK_BIT_LEFT) || (walk->active_directions & WALK_BIT_RIGHT)) { direction = 0; if (walk->active_directions & WALK_BIT_LEFT) { direction += 1; } if (walk->active_directions & WALK_BIT_RIGHT) { direction -= 1; } dvec_tmp[0] = direction * rv3d->viewinv[0][0]; dvec_tmp[1] = direction * rv3d->viewinv[0][1]; dvec_tmp[2] = 0.0f; add_v3_v3(dvec, dvec_tmp); } if ((walk->active_directions & WALK_BIT_UP) || (walk->active_directions & WALK_BIT_DOWN)) { if (walk->navigation_mode == WALK_MODE_FREE) { direction = 0; if (walk->active_directions & WALK_BIT_UP) { direction -= 1; } if (walk->active_directions & WALK_BIT_DOWN) { direction = 1; } copy_v3_fl3(dvec_tmp, 0.0f, 0.0f, direction); add_v3_v3(dvec, dvec_tmp); } } normalize_v3(dvec); /* apply movement */ mul_v3_fl(dvec, walk->speed * time_redraw); } /* stick to the floor */ if (walk->navigation_mode == WALK_MODE_GRAVITY && ELEM(walk->gravity_state, WALK_GRAVITY_STATE_OFF, WALK_GRAVITY_STATE_START)) { bool ret; float ray_distance; float difference = -100.0f; float fall_distance; ret = walk_floor_distance_get(rv3d, walk, dvec, &ray_distance); if (ret) { difference = walk->view_height - ray_distance; } /* the distance we would fall naturally smoothly enough that we * can manually drop the object without activating gravity */ fall_distance = time_redraw * walk->speed * WALK_BOOST_FACTOR; if (fabsf(difference) < fall_distance) { /* slope/stairs */ dvec[2] -= difference; /* in case we switched from FREE to GRAVITY too close to the ground */ if (walk->gravity_state == WALK_GRAVITY_STATE_START) { walk->gravity_state = WALK_GRAVITY_STATE_OFF; } } else { /* hijack the teleport variables */ walk->teleport.initial_time = PIL_check_seconds_timer(); walk->gravity_state = WALK_GRAVITY_STATE_ON; walk->teleport.duration = 0.0f; copy_v3_v3(walk->teleport.origin, walk->rv3d->viewinv[3]); copy_v2_v2(walk->teleport.direction, dvec); } } /* Falling or jumping) */ if (ELEM(walk->gravity_state, WALK_GRAVITY_STATE_ON, WALK_GRAVITY_STATE_JUMP)) { float t; float z_cur, z_new; bool ret; float ray_distance, difference = -100.0f; /* delta time */ t = (float)(PIL_check_seconds_timer() - walk->teleport.initial_time); /* keep moving if we were moving */ copy_v2_v2(dvec, walk->teleport.direction); z_cur = walk->rv3d->viewinv[3][2] / walk->grid; z_new = (walk->teleport.origin[2] / walk->grid) - getFreeFallDistance(walk->gravity, t); /* jump */ z_new += t * walk->speed_jump; /* duration is the jump duration */ if (t > walk->teleport.duration) { /* check to see if we are landing */ ret = walk_floor_distance_get(rv3d, walk, dvec, &ray_distance); if (ret) { difference = walk->view_height - ray_distance; } if (difference > 0.0f) { /* quit falling, lands at "view_height" from the floor */ dvec[2] -= difference; walk->gravity_state = WALK_GRAVITY_STATE_OFF; walk->speed_jump = 0.0f; } else { /* keep falling */ dvec[2] = z_cur - z_new; } } else { /* keep going up (jump) */ dvec[2] = z_cur - z_new; } } /* Teleport */ else if (walk->teleport.state == WALK_TELEPORT_STATE_ON) { float t; /* factor */ float new_loc[3]; float cur_loc[3]; /* linear interpolation */ t = (float)(PIL_check_seconds_timer() - walk->teleport.initial_time); t /= walk->teleport.duration; /* clamp so we don't go past our limit */ if (t >= 1.0f) { t = 1.0f; walk->teleport.state = WALK_TELEPORT_STATE_OFF; walk_navigation_mode_set(walk, walk->teleport.navigation_mode); } mul_v3_v3fl(new_loc, walk->teleport.direction, t); add_v3_v3(new_loc, walk->teleport.origin); copy_v3_v3(cur_loc, walk->rv3d->viewinv[3]); sub_v3_v3v3(dvec, cur_loc, new_loc); } /* scale the movement to the scene size */ mul_v3_v3fl(dvec_tmp, dvec, walk->grid); add_v3_v3(rv3d->ofs, dvec_tmp); if (rv3d->persp == RV3D_CAMOB) { walk->need_rotation_keyframe |= (moffset[0] || moffset[1] || walk->zlock == WALK_AXISLOCK_STATE_ACTIVE); walk->need_translation_keyframe |= (len_squared_v3(dvec_tmp) > FLT_EPSILON); walkMoveCamera( C, walk, walk->need_rotation_keyframe, walk->need_translation_keyframe, is_confirm); } } else { /* we're not redrawing but we need to update the time else the view will jump */ walk->time_lastdraw = PIL_check_seconds_timer(); } /* end drawing */ copy_v3_v3(walk->dvec_prev, dvec); } return OPERATOR_FINISHED; #undef WALK_ROTATE_TABLET_FAC #undef WALK_TOP_LIMIT #undef WALK_BOTTOM_LIMIT #undef WALK_MOVE_SPEED #undef WALK_BOOST_FACTOR } #ifdef WITH_INPUT_NDOF static void walkApply_ndof(bContext *C, WalkInfo *walk, bool is_confirm) { Object *lock_ob = ED_view3d_cameracontrol_object_get(walk->v3d_camera_control); bool has_translate, has_rotate; view3d_ndof_fly(walk->ndof, walk->v3d, walk->rv3d, walk->is_slow, lock_ob ? lock_ob->protectflag : 0, &has_translate, &has_rotate); if (has_translate || has_rotate) { walk->redraw = true; if (walk->rv3d->persp == RV3D_CAMOB) { walk->need_rotation_keyframe |= has_rotate; walk->need_translation_keyframe |= has_translate; walkMoveCamera( C, walk, walk->need_rotation_keyframe, walk->need_translation_keyframe, is_confirm); } } } #endif /* WITH_INPUT_NDOF */ /** \} */ /* -------------------------------------------------------------------- */ /** \name Walk Operator * \{ */ static int walk_invoke(bContext *C, wmOperator *op, const wmEvent *event) { RegionView3D *rv3d = CTX_wm_region_view3d(C); WalkInfo *walk; if (RV3D_LOCK_FLAGS(rv3d) & RV3D_LOCK_ANY_TRANSFORM) { return OPERATOR_CANCELLED; } walk = MEM_callocN(sizeof(WalkInfo), "NavigationWalkOperation"); op->customdata = walk; if (initWalkInfo(C, walk, op, event->mval) == false) { MEM_freeN(op->customdata); return OPERATOR_CANCELLED; } walkEvent(walk, event); WM_event_add_modal_handler(C, op); return OPERATOR_RUNNING_MODAL; } static void walk_cancel(bContext *C, wmOperator *op) { WalkInfo *walk = op->customdata; walk->state = WALK_CANCEL; walkEnd(C, walk); op->customdata = NULL; } static int walk_modal(bContext *C, wmOperator *op, const wmEvent *event) { int exit_code; bool do_draw = false; WalkInfo *walk = op->customdata; View3D *v3d = walk->v3d; RegionView3D *rv3d = walk->rv3d; Object *walk_object = ED_view3d_cameracontrol_object_get(walk->v3d_camera_control); walk->redraw = false; walkEvent(walk, event); #ifdef WITH_INPUT_NDOF if (walk->ndof) { /* 3D mouse overrules [2D mouse + timer] */ if (event->type == NDOF_MOTION) { walkApply_ndof(C, walk, false); } } else #endif /* WITH_INPUT_NDOF */ if (event->type == TIMER && event->customdata == walk->timer) { walkApply(C, walk, false); } do_draw |= walk->redraw; exit_code = walkEnd(C, walk); if (exit_code != OPERATOR_RUNNING_MODAL) { do_draw = true; } if (exit_code == OPERATOR_FINISHED) { ED_view3d_camera_lock_undo_push(op->type->name, v3d, rv3d, C); } if (do_draw) { if (rv3d->persp == RV3D_CAMOB) { WM_event_add_notifier(C, NC_OBJECT | ND_TRANSFORM, walk_object); } /* too frequent, commented with NDOF_WALK_DRAW_TOOMUCH for now */ // puts("redraw!"); ED_region_tag_redraw(CTX_wm_region(C)); } return exit_code; } void VIEW3D_OT_walk(wmOperatorType *ot) { /* identifiers */ ot->name = "Walk Navigation"; ot->description = "Interactively walk around the scene"; ot->idname = "VIEW3D_OT_walk"; /* api callbacks */ ot->invoke = walk_invoke; ot->cancel = walk_cancel; ot->modal = walk_modal; ot->poll = ED_operator_region_view3d_active; /* flags */ /* NOTE: #OPTYPE_BLOCKING isn't used because this needs to grab & hide the cursor. * where as blocking confines the cursor to the window bounds, even when hidden. */ ot->flag = 0; } /** \} */