4
$\begingroup$

is there a simple & efficient way to draw the outline of a mesh with the gpu module ? the script should be able to control the width of the outline line

Defining outline: "A line indicating the outer contours or boundaries of an object or figure."

Example of an outline: enter image description here

Nice article on how outlines are made in games: https://alexanderameye.github.io/notes/rendering-outlines/

$\endgroup$
10
  • $\begingroup$ i searched on gooogle for sample images on "outline" i couldn't find anything to help me understand. did you mean bounding box? perhaps an image of, say suzanne object with an outline would help. $\endgroup$
    – Harry McKenzie
    Commented Sep 4, 2022 at 12:04
  • 1
    $\begingroup$ @HarryMcKenzie In video games at least outlines are done by drawing an object in a solid color and dilating the 2D render, then drawing the actual rendered object on top of it. See answers.unity.com/questions/1292087/… for an example. I agree that a visual example would be helpful here to know exactly what you are looking for as an effect $\endgroup$
    – Gorgious
    Commented Sep 4, 2022 at 12:17
  • $\begingroup$ ohhhh ok yeah that sounds about right, that's probably what the OP meant i guess. so basically the same orange/yellow outline when i select an object in blender $\endgroup$
    – Harry McKenzie
    Commented Sep 4, 2022 at 12:25
  • $\begingroup$ @DB3D thanks for adding an image example. i'd be interested to see an answer for this! $\endgroup$
    – Harry McKenzie
    Commented Sep 4, 2022 at 12:34
  • $\begingroup$ how outline are done in games alexanderameye.github.io/notes/rendering-outlines i think it is quite hard to do this in bpy $\endgroup$
    – Fox
    Commented Sep 4, 2022 at 12:38

3 Answers 3

4
$\begingroup$

I got a partial solution from Carbon2

enter image description here

import bpy
import gpu
import numpy as np
from gpu_extras.batch import batch_for_shader


vertex_shader = '''
in vec3 position;
in vec3 normal;
in vec4 color;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

uniform vec3 camera_location;
uniform float factor1;
uniform float factor2;

out vec4 fcolor;
void main()
{
    
    vec3 pos = vec3(model * vec4(position, 1.0));
    vec3 nor = mat3(transpose(inverse(model))) * normal;
    
    float d = distance(pos, camera_location) * factor1;
    vec3 offset = nor * vec3(d);
    vec3 p = pos + offset;
    //p = pos;
    
    // hmmm?
    //vec3 n = nor * vec3(0.0);
    //nor = nor * vec3(0.0);
    
    vec3 dir = p - camera_location;
    dir = normalize(dir) * vec3(factor2);
    p = p + dir;
    
    //gl_Position = projection * view * model * vec4(position, 1.0);
    //gl_Position = projection * view * model * vec4(p, 1.0);
    gl_Position = projection * view * vec4(p, 1.0);
    //fcolor = color * vec4(offset, 1.0);
    fcolor = color;
    //fcolor = color * vec4(dir / vec3(factor2), 1.0);
}
'''
fragment_shader = '''
in vec4 fcolor;
out vec4 fragColor;
void main()
{
    fragColor = blender_srgb_to_framebuffer_space(fcolor);
}
'''


def draw(self, context, ):
    gpu.state.depth_test_set('LESS')
    
    self.shader.bind()
    self.shader.uniform_float("model", self.o.matrix_world)
    self.shader.uniform_float("view", bpy.context.region_data.view_matrix)
    self.shader.uniform_float("projection", bpy.context.region_data.window_matrix)
    
    cl = bpy.context.region_data.view_matrix.inverted().translation
    self.shader.uniform_float("camera_location", cl)
    self.shader.uniform_float("factor1", 0.01)
    self.shader.uniform_float("factor2", 50.0)
    
    self.batch.draw(self.shader)
    
    gpu.state.depth_test_set('NONE')


class ModalOperator(bpy.types.Operator):
    bl_idname = "view3d.modal_operator"
    bl_label = ""
    
    def prepare(self, ):
        me = self.o.data
        me.calc_loop_triangles()
        
        vs = np.zeros((len(me.vertices) * 3, ), dtype=np.float32, )
        me.vertices.foreach_get('co', vs)
        vs.shape = (-1, 3, )
        ns = np.zeros((len(me.vertices) * 3, ), dtype=np.float32, )
        me.vertices.foreach_get('normal', ns)
        ns.shape = (-1, 3, )
        fs = np.zeros((len(me.loop_triangles) * 3, ), dtype=np.int32, )
        me.loop_triangles.foreach_get('vertices', fs)
        fs.shape = (-1, 3, )
        cs = np.full((len(me.vertices), 4), (1.0, 1.0, 0.0, 1.0), dtype=np.float32, )
        
        shader = gpu.types.GPUShader(vertex_shader, fragment_shader, )
        batch = batch_for_shader(shader, 'TRIS', {"position": vs, "normal": ns, "color": cs, }, indices=fs, )
        return shader, batch
    
    def tag_redraw(self):
        for window in bpy.context.window_manager.windows:
            for area in window.screen.areas:
                if(area.type == 'VIEW_3D'):
                    area.tag_redraw()
    
    def __init__(self):
        print("start")
    
    def __del__(self):
        print("end")
    
    def modal(self, context, event):
        if event.type in {'ESC'}:
            bpy.types.SpaceView3D.draw_handler_remove(self._handle_3d, 'WINDOW')
            self.tag_redraw()
            return {'CANCELLED'}
        else:
            pass
        
        return {'PASS_THROUGH'}
    
    def invoke(self, context, event):
        self.o = context.active_object
        self.shader, self.batch = self.prepare()
        self._handle_3d = bpy.types.SpaceView3D.draw_handler_add(draw, (self, context, ), 'WINDOW', 'POST_VIEW', )
        context.window_manager.modal_handler_add(self)
        self.tag_redraw()
        return {'RUNNING_MODAL'}


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


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


if __name__ == "__main__":
    register()
    bpy.ops.view3d.modal_operator('INVOKE_DEFAULT')

$\endgroup$
3
  • $\begingroup$ why only partial? $\endgroup$
    – Harry McKenzie
    Commented Sep 14, 2022 at 0:04
  • $\begingroup$ because there is no support for many objects, handling accurate behavior for back/front drawing is challenging $\endgroup$
    – Fox
    Commented Sep 14, 2022 at 11:45
  • 1
    $\begingroup$ i tried it, it works actually pretty well. whenever i add a new mesh to the object and run the code, it works perfectly. nice work! $\endgroup$
    – Harry McKenzie
    Commented Sep 15, 2022 at 1:05
2
$\begingroup$

The following script will draw the outline of the selected object in bpy.context.object. Make sure you Apply Transforms before you run the script:

import bpy
import gpu
from gpu_extras.batch import batch_for_shader

o = bpy.context.object
mesh = o.data
coords = [v.co + o.location for v in mesh.vertices]
indices = [(e.vertices[0], e.vertices[1]) for e in mesh.edges]
shader = gpu.shader.from_builtin('3D_UNIFORM_COLOR')
batch = batch_for_shader(
shader, 'LINES', {"pos": coords}, indices=indices)

def draw():
    shader.bind()
    shader.uniform_float("color", (1, 0, 0, 1))
    batch.draw(shader)

bpy.types.SpaceView3D.draw_handler_add(draw, (), 'WINDOW', 'POST_VIEW')

Here's the outline of the Suzanne monkey object. Please see the documentation on GPU Module for more information.

enter image description here

$\endgroup$
3
  • 2
    $\begingroup$ Thank you very much! I believe you misinterpreted what an outline is, am i wrong? $\endgroup$
    – Fox
    Commented Sep 4, 2022 at 11:49
  • $\begingroup$ oh i was thinking of wireframe sorry. hmmm i'm not sure what you mean. do you mean like the bounding box? maybe an image would help with the description of outline "A line indicating the outer contours or boundaries of an object or figure." $\endgroup$
    – Harry McKenzie
    Commented Sep 4, 2022 at 12:01
  • 2
    $\begingroup$ anyway, don't worry...both answers are incorrect :D $\endgroup$
    – Chris
    Commented Sep 8, 2022 at 15:25
0
$\begingroup$

It's not a script, but you could tweek this shader.enter image description here

$\endgroup$

You must log in to answer this question.

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