0
$\begingroup$

I would like to constraint an object (cube) to the viewport camera, so it will be always in user's field of view. Is it possible with constraints (I didn't find any way) ? I also tried with python to get 3d coords :

import bpy
from bpy.props import IntProperty, FloatProperty
import bpy_extras
from mathutils import  Vector, geometry
import mathutils

class ModalOperator(bpy.types.Operator):
    """Move an object with the mouse, example"""
    bl_idname = "object.modal_operator"
    bl_label = "Simple Modal Operator"

    first_mouse_x: IntProperty()
    first_value: FloatProperty()

    def modal(self, context, event):
        if event.type in {'MIDDLEMOUSE', 'WHEELUPMOUSE', 'WHEELDOWNMOUSE'}:
            coord = self.get_view_coord(context)
            print("coord = ", coord)
            return {'PASS_THROUGH'}
        

        elif event.type in {'RIGHTMOUSE', 'ESC'}:
            context.object.location.x = self.first_value
            return {'CANCELLED'}

        return {'RUNNING_MODAL'}

    def invoke(self, context, event):
        context.window_manager.modal_handler_add(self)
        return {'RUNNING_MODAL'}

        
    def get_view_coord(self,context):
        region = bpy.context.region
        for area in bpy.context.screen.areas:
            if area.type == 'VIEW_3D':
                rv3d = area.spaces.active.region_3d
                break

        pos = bpy_extras.view3d_utils.region_2d_to_location_3d( region, rv3d, Vector((100,500)), Vector((0,0,0)) )
        orient = bpy_extras.view3d_utils.region_2d_to_vector_3d( region, rv3d, Vector((100,500)))
        
        bpy.ops.object.empty_add(type='ARROWS', location= pos)
        
        bpy.context.object.rotation_mode = 'QUATERNION'
        bpy.context.object.rotation_quaternion = orient.to_track_quat('Z','Y')
        return pos
        

def register():
    bpy.utils.register_class(ModalOperator)


def unregister():
    bpy.utils.unregister_class(ModalOperator)


if __name__ == "__main__":
    register()

    # test call
    bpy.ops.object.modal_operator('INVOKE_DEFAULT')

Edit 1 : The final goal is to have several controller objects on the sides of 3d viewport, so I can manipulate them and via handlers, tweaking previously generated curves.

Edit 2 : With this modal example, I can have an empty placed in 3d viewport, but still don't know how to follow camera, how to get height and width of 3d area (to place it at the top left corner for example), plus Z axis is well oriented, but I would like Y axis to be parallel to the border of the screen.

$\endgroup$
4
  • $\begingroup$ Have a script that "constrains" (by transforming) an object to align (face towards) and be in the center of the largest 3d view in screen via a modal timer operator... which would I suppose go some way to answer Q as titles. The view_matrix belongs to the 3d region of active space of a 3d view area ie if a is area of type ``VIEW_3D'` then a.spaces.active.region_3d.view_matrix whereas error is telling you context.region_data is None and hence has no properties. Suggest editing to clarify the desired result, should object of interest face view, fill view, be in middle of view.. $\endgroup$
    – batFINGER
    Commented Apr 11, 2021 at 19:00
  • $\begingroup$ Please edit any extra information into question. $\endgroup$
    – batFINGER
    Commented Apr 11, 2021 at 19:37
  • $\begingroup$ was thinking perhaps a slightly more "shorter term" goal relating to "How to constrain object to viewport" $\endgroup$
    – batFINGER
    Commented Apr 12, 2021 at 6:07
  • $\begingroup$ Thanks for the modal idea ; I now understand what you mean with the "clarify the desired results" by testing it : edit 2 plus new code sample $\endgroup$
    – Tolgan
    Commented Apr 12, 2021 at 21:28

1 Answer 1

1
$\begingroup$

Keep context object centered on largest viewport in screen.

enter image description here

The view is set up similarly to the camera, ie it looks down its $-Z$ axis.

Rotate an object to camera view

Access view orientation from python?

whereas meshes are normally $Z$ up $-Y$ forward.

The view_align method below, looks for the largest 3d view, then using its matrix aligns the context object such that it looks directly at the view, from 5 global units away.

The rest is basically the modal timer operator template, similarly to question

import bpy
from mathutils import Vector, Matrix
from bpy_extras.io_utils import axis_conversion

distance_from_view = 5

A = axis_conversion(
        from_forward='Y',
        from_up='Z',
        to_forward='-Z',
        to_up='Y').to_4x4()

def view_align(context):
    areas =  sorted(
            [
                a 
                for a in context.screen.areas 
                if a.type == 'VIEW_3D'
            ], 
            key=lambda a : a.width * a.height
        )
        
    if areas:
        screen = context.screen
        ob = context.object
        mw = ob.matrix_world
        r3d = areas.pop().spaces.active.region_3d
        M = r3d.view_matrix
        V = M.inverted()
        # maintain global distance from camera
        T = Matrix.Translation(V @ Vector((0, 0, -distance_from_view)))
        # keep original transform align to view
        #T = Matrix.Translation(mw.translation)
        R = M.to_3x3().transposed().to_4x4()
        S = Matrix.Diagonal(mw.to_scale()).to_4x4()
        ob.matrix_world = T @ (R @ A) @ S
        
        
class ModalTimerOperator(bpy.types.Operator):
    """Operator which runs its self from a timer"""
    bl_idname = "wm.modal_timer_operator"
    bl_label = "Modal Timer Operator"

    _timer = None

    def modal(self, context, event):
        if event.type in {'RIGHTMOUSE', 'ESC'}:
            self.cancel(context)
            return {'CANCELLED'}

        if event.type == 'TIMER':
            view_align(context)

        return {'PASS_THROUGH'}

    def execute(self, context):
        wm = context.window_manager
        self._timer = wm.event_timer_add(0.01, window=context.window)
        wm.modal_handler_add(self)
        return {'RUNNING_MODAL'}

    def cancel(self, context):
        wm = context.window_manager
        wm.event_timer_remove(self._timer)


def register():
    bpy.utils.register_class(ModalTimerOperator)


def unregister():
    bpy.utils.unregister_class(ModalTimerOperator)


if __name__ == "__main__":
    register()

    # test call
    bpy.ops.wm.modal_timer_operator()
$\endgroup$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .