Passing a custom camera view to gpencil mesh convert

I am trying to learn how to use the Python API for Blender, and have been wanting to see if I can make some kind of gpencil to mesh convert tool to see if i can make it do consistent conversions through a specific camera view.

I was able to come up with this so far based on what I could find across various searches:

import bpy


def get_3d_view_custom_context():
    """creates a 3d view custom context

    Args:
        None

    Returns:
        custom_context (dict): the dictionary creating a custom context
    """
    my_camera = bpy.context.scene.cameraComboBox
    print('my_camera', my_camera)
    if my_camera == 'no_items':
        return

    custom_context = bpy.context.copy()

    for area in bpy.context.screen.areas:
        if area.type == "VIEW_3D":
            for region in area.regions:
                if region.type == "WINDOW":
                    custom_context["area"] = area
                    # view_3d = area.spaces.active
                    # area.spaces.active.local_view = bpy.context.scene.objects[my_camera]
                    # view_3d.camera_to_view()
                    custom_context["region"] = region
                    break
            break
    # bpy.context.scene.camera = bpy.data.objects[my_camera]
    # areas3d  = [area for area in bpy.context.window.screen.areas if area.type == 'VIEW_3D']
    # bpy.ops.view3d.camera_to_view({'area':areas3d[0]})
    print("Custom context use with VIEW_3D area and WINDOW region.")
    custom_context['scene'].camera = bpy.context.scene.objects[my_camera]
    # bpy.ops.view_3d.camera_to_view()
    custom_context['space_data'].region_3d.view_perspective == 'CAMERA'
    return custom_context



class ConvertToMeshFunc(bpy.types.Operator):
    """Convert gpencil to geo"""

    bl_idname = "gpencil.blendlines_meshify"
    bl_label = "Convert to Poly"
    bl_context = "gpencil"
    bl_options = {"REGISTER", "UNDO"}

    def execute(self, context):
        obj = context.object
        override = get_3d_view_custom_context()
        bpy.ops.object.gpencil_modifier_apply(modifier="Noise")
        obj_world_scale = obj.matrix_world.to_scale()
        depth = 0.005 * obj.data.pixel_factor * min(obj_world_scale)
        resolution = 12

        bpy.ops.gpencil.convert(type="POLY",use_timing_data=False)
        bpy.ops.object.convert(target="MESH")
        active = context.view_layer.objects.active
        selected = context.view_layer.objects.selected
        # objs = [item for item in selected if item != active]
        # converted = objs[0]
        # print("{} converted to {}.".format(obj.name, converted.name))
        return {"FINISHED"}

    @classmethod
    def poll(cls, context):
        return context.mode in {"OBJECT"}

class GPencilToMesh(bpy.types.Operator):
    """Convert gpencil to mesh, cap the ends, cut out gpmask, add material/stroke attributes"""

    bl_idname = "object.mesh_check"
    bl_label = "GPencil to Mesh"
    bl_context = "object"
    bl_options = {"REGISTER", "UNDO"}

    def execute(self, context):
        bpy.ops.gpencil.blendlines_meshify()
        return {"FINISHED"}

    @classmethod
    def poll(cls, context):
        return context.mode in {"OBJECT"}

class UpdateCamerasList(bpy.types.Operator):
    bl_idname = "object.greasepencil_camsrefresh"
    bl_label = "Refresh Cameras"
    bl_context = "object"
    bl_options = {"REGISTER", "UNDO"}
    
    def execute(self, context):
        cams_list = [ob for ob in bpy.context.scene.objects if ob.type == 'CAMERA']
        cams_combo_items = []
        if cams_list:
            for idx, cam in enumerate(cams_list):
                cams_combo_items.append((cam.name, cam.name, "", idx+1))
            self.updateCameraDropdown(cams_combo_items)
        print('cams_combo_items:', cams_combo_items)

        return {'FINISHED'}

    @classmethod
    def updateCameraDropdown(cls, cItems=None):
        if not cItems:
            cItems = [
                ("no_items", "No items", "", 1),
            ]
            # cItems = [(bpy.data.objects['Camera'], 'Camera', '', 1)]
            default_selection = "no_items"
        else:
            default_selection = bpy.data.scenes["Scene"].camera.name
        bpy.types.Scene.cameraComboBox = bpy.props.EnumProperty(
                                            items=cItems,
                                            name="Cameras",
                                            description="Camera",
                                            default=default_selection,
                                        )
        # print('get_3d_view_custom_context():', get_3d_view_custom_context())
        # my_camera = bpy.context.scene.cameraComboBox
        # bpy.context.scene.camera = bpy.data.objects[my_camera]
        # areas3d  = [area for area in bpy.context.window.screen.areas if area.type == 'VIEW_3D']
        # bpy.ops.view3d.camera_to_view({'area':areas3d[0]})
        get_3d_view_custom_context()


class VIEW3D_PT_greasepencil_mesh_test(bpy.types.Panel):
    """BlendLines Export UI Panel"""

    bl_space_type = "VIEW_3D"
    bl_region_type = "UI"
    bl_idname = "VIEW3D_PT_greasepencil_mesh_test"
    bl_category = "GREASEPENCILTEST"
    bl_label = "testing meshify for greasepencil"
    
    def draw(self, context):
        layout = self.layout
        scene = bpy.context.scene
        box = layout.box()
        row = box.row()
        row.operator(
            "object.mesh_check",
            text="Mesh Check",
            icon="MESH_ICOSPHERE",
        )
        box1 = layout.box()
        row1 = box1.row()
        row1.label(text="Set Camera:")
        row2 = box1.row()
        row2.prop(scene, "cameraComboBox", text="")
        row3 = box1.row()
        row3.operator(
            "object.greasepencil_camsrefresh",
            text="Refresh Cameras",
            )


# Registration
# =========================================================================
classes = (
    ConvertToMeshFunc,
    GPencilToMesh,
    UpdateCamerasList,
    VIEW3D_PT_greasepencil_mesh_test,
)


def register():
    UpdateCamerasList.updateCameraDropdown()
    for cls in classes:
        bpy.utils.register_class(cls)

def unregister():
    for cls in reversed(classes):
        if cls.is_registered:
            bpy.utils.unregister_class(cls)
    del bpy.types.Scene.cameraComboBox

if __name__ == "__main__":
    register()

this seems to just create a mesh based on the current viewport I am looking through which is great, but what I really want to do is pass a view from a specific camera somewhere in bpy.ops.gpencil.convert(type="POLY",use_timing_data=False), but when I pass the result of get_3d_view_custom_context() this errors out.

I am still having troubles understanding what this “context” stuff is so I am pretty stuck as of now. Would someone be able to provide some insight as to how to make sure this mesh conversion ONLY WORKS from a specific camera view…?

i jusr want to follow up on this to see if anyone has any insights…

So I’m not sure how to do what you’re trying do, but maybe I can help some with understanding what the API actually does. (Are you trying to make your operator only work if the grease pencil is visible to a selected camera? That could be more complicated than it sounds. I’m not seeing an easy API solution for that. It seems like what you would have to do is write or find code that does the math to check if an object is in the camera view.)

The documentation says bpy.ops.gpencil.convert converts the active grease pencil object to a Bézier curve. Which grease pencil is active doesn’t have anything to do with the viewport you’re looking through. It is just the last selected object. So there’s no way to pass a camera view to it.

context is a way to reference a lot of the data, especially state dependent data like what objects are currently selected. There is a lot there. I think as an introduction to it just know that if you’re looking for data it will often be referend through context. The list of selected objects can be found using context.selected_objects as a random example.

The context also has information about what area and mode you are in. Context overrides are useful if you need to do an operation in a different area. I don’t think it is useful in your case though because your panel is already in the 3D Viewport. (Might be where some of the confusion is coming from. A context for the 3D Viewport works for operations in the 3D Viewport but it doesn’t have anything to do with the view you are looking through. It just works in the 3D Viewport space. If you tried to do something say in the Graph Editor from your operator, then you might get a context is incorrect error because it is using a context for the 3D Viewport, not the Graph Editor.)

1 Like

Hello! thank you for the response and explaining some of the API details.

To clarify, from what I understand the gpencil’s convert() feature allows you to convert the created strokes into meshes, but this is always dependent on the perspective view of the active viewport.

What I would like to do, is REGARDLESS of what camera your current viewport is viewing from, to make sure this convert() process only happens through a very specific camera (ie: a render camera that you have established framing and will not be moving)

so if it is the case that gpencil does not allow a camera to be passed, i suppose an alternative method is to temporarily create a brand new viewport that is viewed from the desired camera, and then run gpencil.convert() from that viewport or context whatever the terminology is in Blender. but I am not sure if this is possible in Blender…

(edit: made updates after discovering a == that needed to be =)

Ah that helps me understand better what you’re trying to do. It may be due to my limited knowledge of grease pencil, but I don’t know what about convert() is dependent on the current viewport?

But you can use your custom context with a different camera do to operations with a context override:

# this is all you need to make the context. the area is already set up for you. pass in the context from the operator's execute method
def get_3d_view_custom_context(context):
    my_camera = context.scene.cameraComboBox
    print('my_camera', my_camera)
    if my_camera == 'no_items':
        return None

    custom_context = context.copy()
    custom_context['scene'].camera = context.scene.objects[my_camera]

    custom_context['space_data'].region_3d.view_perspective = 'CAMERA'
    return custom_context

Then when you go to call convert you can use the the custom context to override the active camera. This does move the actual view to the camera and back. Not sure how to avoid that.

        override = get_3d_view_custom_context(context)
        if override is not None:
            with context.temp_override(**override):
                < conversion logic here >

thank you!

and sorry i come from a Maya background so it may just be that blender operates way differently

the reason why the gpencil convert is dependent on the view is because it works like a projection so if certain parts of the stroke is not “visible” in camera view then the resulting mesh may have holes because that part is not seen in the camera

when you say “move the view to the camera and back”, are you referring to a camera view and a perspective view? what if i say had a camera1 and camera2 and i had my view set at camera2 but my render camera is camera1, would this put the view to camera1 then back to camera2 instead of perspective?

Yes, bpy.ops.view3d.view_camera() toggles between camera and perspective.

Actually after looking at it some] bpy.ops.view3d.view_camera() might not be needed at all. Because the context override has the view_perspective set to CAMERA already.
This line needs to have a = and not a ==:

custom_context['space_data'].region_3d.view_perspective = 'CAMERA'
def get_3d_view_custom_context(context):
    my_camera = context.scene.cameraComboBox
    print('my_camera', my_camera)
    if my_camera == 'no_items':
        return None

    custom_context = context.copy()
    custom_context['scene'].camera = context.scene.objects[my_camera]
    custom_context['space_data'].region_3d.view_perspective = 'CAMERA'
    return custom_context

…

        override = get_3d_view_custom_context(context)
        if override is not None:
            with context.temp_override(**override):
                < conversion logic here >

okay so then it sounds like if you alreadh have a camera set then theres no real way to “temporarily switch” to another camera and back to the original again?

like again for example if in my scene i am looking through camera2 and i run this convert feature but want to do it through camera1, theres no real way to switch to camera1 and back to camera2 in the viewport?

i suppose some hacky thing can be done to keep reassigning the camera to the desired camera back and forth if thats the limitation of blender

I think the context override here does do a temporary switch. Using your example, If my_camera is camera1, then
custom_context['scene'].camera = context.scene.objects[my_camera]
sets the camera to camera1 in the overridden context.
Once the code exits the with context.temp_override(**override): block everything it will be back to the original context where the camera is still camera2.