6

How can I draw the outline of an object on top of any other object in Qt3D? For instance to highlight a selected object in a 3D editor?

dragly
  • 1,445
  • 11
  • 14

1 Answers1

9

If you want to draw the outline of an entity at all times, even if the entity is behind other entities, one solution is to do it in two steps:

  1. Draw everything as normal.
  2. Draw only the outline of the selected object.

When drawing the outline, you need to use an outline effect, which can be implemented in two render passes:

  1. Render the geometry to a texture using a simple color shader.
  2. Render to screen using a shader that takes each pixel in the texture and compares the surrounding pixels. If they are equal, we are inside the object and the fragment can be discarded. If they differ, we are on the edge of the object and we should draw the color.

Here is a simple implementation of the above-mentioned shader:

#version 150

uniform sampler2D color;
uniform vec2 winSize;

out vec4 fragColor;

void main()
{

    int lineWidth = 5;

    vec2 texCoord = gl_FragCoord.xy / winSize;
    vec2 texCoordUp = (gl_FragCoord.xy + vec2(0, lineWidth)) / winSize;
    vec2 texCoordDown = (gl_FragCoord.xy + vec2(0, -lineWidth)) / winSize;
    vec2 texCoordRight = (gl_FragCoord.xy + vec2(lineWidth, 0)) / winSize;
    vec2 texCoordLeft = (gl_FragCoord.xy + vec2(-lineWidth, 0)) / winSize;

    vec4 col = texture(color, texCoord);
    vec4 colUp = texture(color, texCoordUp);
    vec4 colDown = texture(color, texCoordDown);
    vec4 colRight = texture(color, texCoordRight);
    vec4 colLeft = texture(color, texCoordLeft);

    if ((colUp == colDown && colRight == colLeft) || col.a == 0.0)
        discard;

    fragColor = col;
}

Note: It might be a better idea to take the difference between the values instead of using an equality.

With this method, you don't have to worry about depth testing and the order in which the objects are drawn: The second time you draw, you will always draw on top of everything else.

You could do this by adding a single effect with two techniques with different filter keys. Alternatively, if you want to use materials from Qt3D.Extras, you can add another entity with the same transform and mesh and a material that uses the outline technique.

Here is an example that draws the outline on top of everything else using two render passes:

import QtQuick 2.2 as QQ2
import Qt3D.Core 2.0
import Qt3D.Render 2.0
import Qt3D.Input 2.0
import Qt3D.Extras 2.0

Entity {
    Camera {
        id: camera
        projectionType: CameraLens.PerspectiveProjection
        fieldOfView: 45
        aspectRatio: 16/9
        nearPlane : 0.1
        farPlane : 1000.0
        position: Qt.vector3d( 0.0, 0.0, -40.0 )
        upVector: Qt.vector3d( 0.0, 1.0, 0.0 )
        viewCenter: Qt.vector3d( 0.0, 0.0, 0.0 )
    }

    OrbitCameraController {
        camera: camera
    }

    components: [
        RenderSettings {
            activeFrameGraph: RenderSurfaceSelector {
                id: surfaceSelector
                Viewport {
                    CameraSelector {
                        camera: camera
                        FrustumCulling {
                            TechniqueFilter {
                                matchAll: [
                                    FilterKey { name: "renderingStyle"; value: "forward" }
                                ]
                                ClearBuffers {
                                    clearColor: Qt.rgba(0.1, 0.2, 0.3)
                                    buffers: ClearBuffers.ColorDepthStencilBuffer
                                }
                            }
                            TechniqueFilter {
                                matchAll: [
                                    FilterKey { name: "renderingStyle"; value: "outline" }
                                ]
                                RenderPassFilter {
                                    matchAny: [
                                        FilterKey {
                                            name: "pass"; value: "geometry"
                                        }
                                    ]
                                    ClearBuffers {
                                        buffers: ClearBuffers.ColorDepthStencilBuffer
                                        RenderTargetSelector {
                                            target: RenderTarget {
                                                attachments : [
                                                    RenderTargetOutput {
                                                        objectName : "color"
                                                        attachmentPoint : RenderTargetOutput.Color0
                                                        texture : Texture2D {
                                                            id : colorAttachment
                                                            width : surfaceSelector.surface.width
                                                            height : surfaceSelector.surface.height
                                                            format : Texture.RGBA32F
                                                        }
                                                    }
                                                ]
                                            }
                                        }
                                    }
                                }
                                RenderPassFilter {
                                    parameters: [
                                        Parameter { name: "color"; value: colorAttachment },
                                        Parameter { name: "winSize"; value : Qt.size(surfaceSelector.surface.width, surfaceSelector.surface.height) }
                                    ]
                                    matchAny: [
                                        FilterKey {
                                            name: "pass"; value: "outline"
                                        }
                                    ]
                                }
                            }
                        }
                    }
                }
            }
        },
        InputSettings { }
    ]

    PhongMaterial {
        id: material
    }

    Material {
        id: outlineMaterial

        effect: Effect {
            techniques: [
                Technique {
                    graphicsApiFilter {
                        api: GraphicsApiFilter.OpenGL
                        majorVersion: 3
                        minorVersion: 1
                        profile: GraphicsApiFilter.CoreProfile
                    }

                    filterKeys: [
                        FilterKey { name: "renderingStyle"; value: "outline" }
                    ]
                    renderPasses: [
                        RenderPass {
                            filterKeys: [
                                FilterKey { name: "pass"; value: "geometry" }
                            ]
                            shaderProgram: ShaderProgram {
                                vertexShaderCode: "
#version 150 core

in vec3 vertexPosition;

uniform mat4 modelViewProjection;

void main()
{
    gl_Position = modelViewProjection * vec4( vertexPosition, 1.0 );
}
"

                                fragmentShaderCode: "
#version 150 core

out vec4 fragColor;

void main()
{
    fragColor = vec4( 1.0, 0.0, 0.0, 1.0 );
}
"
                            }
                        }
                    ]
                }
            ]
        }
    }

    SphereMesh {
        id: sphereMesh
        radius: 3
    }

    Transform {
        id: sphereTransform
    }

    Transform {
        id: sphereTransform2
        // TODO workaround because the transform cannot be shared
        matrix: sphereTransform.matrix
    }

    Entity {
        id: sphereEntity
        components: [ sphereMesh, material, sphereTransform ]
    }

    Entity {
        id: sphereOutlineEntity
        components: [ sphereMesh, outlineMaterial, sphereTransform2 ]
    }

    Entity {
        id: outlineQuad
        components: [
            PlaneMesh {
                width: 2.0
                height: 2.0
                meshResolution: Qt.size(2, 2)
            },
            Transform {
                rotation: fromAxisAndAngle(Qt.vector3d(1, 0, 0), 90)
            },
            Material {
                effect: Effect {
                    techniques: [
                        Technique {
                            filterKeys: [
                                FilterKey { name: "renderingStyle"; value: "outline" }
                            ]
                            graphicsApiFilter {
                                api: GraphicsApiFilter.OpenGL
                                profile: GraphicsApiFilter.CoreProfile
                                majorVersion: 3
                                minorVersion: 1
                            }
                            renderPasses : RenderPass {
                                filterKeys : FilterKey { name : "pass"; value : "outline" }
                                shaderProgram : ShaderProgram {
                                    vertexShaderCode: "
#version 150

in vec4 vertexPosition;
uniform mat4 modelMatrix;

void main()
{
    gl_Position = modelMatrix * vertexPosition;
}
"

                                    fragmentShaderCode: "
#version 150

uniform sampler2D color;
uniform vec2 winSize;

out vec4 fragColor;

void main()
{

    int lineWidth = 5;

    vec2 texCoord = gl_FragCoord.xy / winSize;
    vec2 texCoordUp = (gl_FragCoord.xy + vec2(0, lineWidth)) / winSize;
    vec2 texCoordDown = (gl_FragCoord.xy + vec2(0, -lineWidth)) / winSize;
    vec2 texCoordRight = (gl_FragCoord.xy + vec2(lineWidth, 0)) / winSize;
    vec2 texCoordLeft = (gl_FragCoord.xy + vec2(-lineWidth, 0)) / winSize;

    vec4 col = texture(color, texCoord);
    vec4 colUp = texture(color, texCoordUp);
    vec4 colDown = texture(color, texCoordDown);
    vec4 colRight = texture(color, texCoordRight);
    vec4 colLeft = texture(color, texCoordLeft);

    if ((colUp == colDown && colRight == colLeft) || col.a == 0.0)
        discard;

    fragColor = col;
}
"
                                }
                            }
                        }]
                }
            }

        ]
    }
}

The result:

Sphere rendered with outline

dragly
  • 1,445
  • 11
  • 14
  • 1
    You can create a [stencil](https://www.khronos.org/opengl/wiki/Stencil_Test) mask when rendering the original geometry. This mask can be used to create the outline. So the extra pass, which renders the geometry to texture can be skipped. – Rabbid76 Sep 29 '18 at 13:16
  • Yes, that's a good alternative. It really depends on what you want to outline. What you describe will highlight physical outlines, including those within the same object, which for instance is great for cartoon-like effects. The above answer gives an effect closer to the outlines drawn when an object is selected in 3D editors like Blender or games like Age of Empires. If two outlined objects are overlapping, the above solution will draw the outline as if the two objects were the same object, which sometimes is what you want. – dragly Sep 29 '18 at 13:17
  • 1
    The stencil test can be set in that way, that the mask would contain the same information, as the texture in step 1) of your answer. – Rabbid76 Sep 29 '18 at 13:19
  • Good point. That would make the test for discarding a bit more rigorous. Or were you thinking of a different benefit by using the stencil instead of another texture? – dragly Sep 29 '18 at 13:44
  • This example doesn't seem to work in Qt 5.12, with an error `TypeError: Cannot read property 'height' of null` accessing `width`/`height` properties of `surfaceSelector.surface` – fferri Nov 07 '18 at 07:54
  • Beside that error, it works, but render is incorrect on macOS: https://postimg.cc/VdhPG2MR – fferri Nov 07 '18 at 08:24
  • 1
    That is because the expression is evaluated before the `surfaceSelector.surface` is initialized. To get around it, you can change it to `surfaceSelector.surface ? surfaceSelector.height : 100` (or some other number you prefer as a default). Regarding the offset, that is because the texture size does not take into account the Retina display on your Mac. To fix it, you can multiply the width and height with `Screen.devicePixelRatio`. – dragly Nov 15 '18 at 17:54
  • Tried again today to implement this code snippet using Qt 5.13.1, and the material won't render at all. Did I make some obvious mistake [here (git repo link)](https://github.com/fferri/qt3d-object-selection/tree/fec4f30814e1eb8f15eb2bb429fdd1dda9f409ea)? – fferri Nov 27 '19 at 16:31
  • @fferri You appear to be using two materials on one entity, which does not work. Instead, you need to use the outlineMaterial on a quad that fills the screen. As mentioned above, you need to render in two passes: One to a texture and one that uses the texture (on the quad). This is the deferred rendering pattern. There is some information about it in the docs: https://doc.qt.io/qt-5/qt3drender-framegraph.html#deferred-renderer and https://doc.qt.io/archives/qt-5.6/qt3d-deferred-renderer-qml-example.html – dragly Dec 01 '19 at 14:27