3

I draw some 2d text entities in a Qt3D QML scene but some of the texts always render opaque, i.e hide the contents behind them. When looking at the scene from behind ( changing the position of the camera to Qt.vector3d(0,0,-40) ) all texts render OK.

The following image shows the wrong behaviour, I would expect the text "AAARGH" not to be rendered on a white background, but the green text shining through.

WrongRendering

Platform is Windows 64-bit, Qt5.13.0, and Visual Studio 2019.

See the following small example that demonstrates the issue:

BrokenEntity.qml

import Qt3D.Core 2.0
import Qt3D.Render 2.0
import Qt3D.Input 2.0
import Qt3D.Extras 2.13

import QtQuick 2.0 as QQ2

Entity {
    id: sceneRoot

    Camera {
        id: camera
        projectionType: CameraLens.PerspectiveProjection
        fieldOfView: 45
        nearPlane : 0.1
        farPlane : 1000.0
        position: Qt.vector3d( 0.0, 0.0, 40 )
        upVector: Qt.vector3d( 0.0, 1.0, 0.0 )
        viewCenter: Qt.vector3d( 0.0, 0.0, 0.0 )
    }

    OrbitCameraController { camera: camera }

    components: [
        RenderSettings {
            activeFrameGraph: ForwardRenderer {
                camera: camera
                clearColor: "transparent"
            }
        },

        InputSettings { }
    ]

    Entity {
        components: [ Transform { translation: Qt.vector3d(-12.5,-5,-20) } ]
        Text2DEntity {
            font.family: "Sans Serif"
            font.pointSize: 5
            color: Qt.rgba(0, 0, 1, 0.5)
            text: "AAARGH"
            width: text.length * font.pointSize
            height: font.pointSize * 1.2
        }
    }
    Entity {
        PhongMaterial {
            id: material
            ambient: Qt.rgba(1, 1, 0, 1)
            diffuse: Qt.rgba(1, 1, 0, 1)
        }
        SphereMesh {
            id: sphereMesh
            radius: 1
            rings: 50
            slices: 50
        }
        Transform {
            id: sphereTransform
            translation: Qt.vector3d(0,0,-25)
            scale3D: Qt.vector3d(1, 1, 1)
        }
        components: [ sphereMesh, material, sphereTransform ]
    }
    Entity {
        components: [ Transform { translation: Qt.vector3d(-25,-5,-30) } ]
        Text2DEntity {
            font.family: "Sans Serif"
            font.pointSize: 10
            color: Qt.rgba(0, 1, 0, 1.0)
            text: "BBBRGH"
            width: text.length * font.pointSize
            height: font.pointSize * 1.2
        }
    }
}

main.qml

import QtQuick 2.0
import QtQuick.Scene3D 2.0

Item {
    Rectangle {
        id: scene
        anchors.fill: parent
        anchors.margins: 50
        color: "white"

        Scene3D {
            id: scene3d
            anchors.fill: parent
            anchors.margins: 10
            focus: true
            aspects: ["input", "logic"]
            cameraAspectRatioMode: Scene3D.AutomaticAspectRatio

            BrokenEntity {}
        }
    }
}

main.cpp

#include <QGuiApplication>
#include <QQuickView>

int main(int argc, char **argv)
{
    QGuiApplication app(argc, argv);

    QQuickView view;

    view.resize(500, 500);
    view.setResizeMode(QQuickView::SizeRootObjectToView);
    view.setSource(QUrl("qrc:/main.qml"));
    view.show();

    return app.exec();
}

I suppose the reason of this behaviour comes from the GLSL shaders (distancefieldtext.vert and distancefieldtext.frag) that are used in Qt3D to render the Text2dEntity.

See attached shader sources.

distancefieldtext.vert

#version 150 core

in vec3 vertexPosition;
in vec2 vertexTexCoord;

out vec2 texCoord;
out float zValue;

uniform mat4 modelView;
uniform mat4 mvp;

void main()
{
    texCoord = vertexTexCoord;
    zValue = vertexPosition.z;

    gl_Position = mvp * vec4(vertexPosition.xy, 0.0, 1.0);
}

distancefieldtext.frag

#version 150 core

uniform sampler2D distanceFieldTexture;
uniform float minAlpha;
uniform float maxAlpha;
uniform float textureSize;
uniform vec4 color;

in vec2 texCoord;
in float zValue;

out vec4 fragColor;

void main()
{
    // determine the scale of the glyph texture within pixel-space coordinates
    // (that is, how many pixels are drawn for each texel)
    vec2 texelDeltaX = abs(dFdx(texCoord));
    vec2 texelDeltaY = abs(dFdy(texCoord));
    float avgTexelDelta = textureSize * 0.5 * (texelDeltaX.x + texelDeltaX.y + texelDeltaY.x + texelDeltaY.y);
    float texScale = 1.0 / avgTexelDelta;

    // scaled to interval [0.0, 0.15]
    float devScaleMin = 0.00;
    float devScaleMax = 0.15;
    float scaled = (clamp(texScale, devScaleMin, devScaleMax) - devScaleMin) / (devScaleMax - devScaleMin);

    // thickness of glyphs should increase a lot for very small glyphs to make them readable
    float base = 0.5;
    float threshold = base * scaled;
    float range = 0.06 / texScale;

    float minAlpha = threshold - range;
    float maxAlpha = threshold + range;

    float distVal = texture(distanceFieldTexture, texCoord).r;
    fragColor = color * smoothstep(minAlpha, maxAlpha, distVal);
    gl_FragDepth = gl_FragCoord.z - zValue * 0.00001;
}

Any ideas on this how to make Qt3D render the text2DEntities with the text itself opaque and the spaces between the text transparant, independent on the viewing direction? Thanks in advance.

Edit

I must have inadvertently changed something in the example, as changing the position of the camera does not show the expected behaviour anymore. I will correct that on Monday when I have access to my work environment.

Update

As I needed double sided lighting for my entities, I had an additional CullFace component with NoCulling added to the RenderStateSet, that explains this behaviour. My FrameGraph was looking like this:

components: [
    RenderSettings {
        activeFrameGraph: RenderStateSet {
            renderStates: [
                CullFace { mode: CullFace.NoCulling }
            ]
            ForwardRenderer {
                camera: camera
                clearColor: "transparent"
            }
        }
    },

    InputSettings { }
]

When viewing from the back side, as the entities were defined from back to front the rendering was correct. This is stated explicitly in the documentation of SortPolicy:

"If QSortPolicy is not present in the FrameGraph, entities are drawn in the order they appear in the entity hierarchy."

When adding an additional SortPolicy component with BackToFront to the FrameGraph the rendering was correct independent of the viewing direction. The FrameGraph then looked like this:

components: [
    RenderSettings {
        activeFrameGraph: SortPolicy {
            sortTypes: [ SortPolicy.BackToFront ]
            RenderStateSet {
                renderStates: [
                    CullFace { mode: CullFace.NoCulling }
                ]
                ForwardRenderer {
                    camera: camera
                    clearColor: "transparent"
                }
            }
        }
    },

    InputSettings { }
]
vre
  • 6,041
  • 1
  • 25
  • 39
  • I doubt the Qt3D shaders are the cause for this. When running the distancefield example from the manual tests, the text is rendered ok, meaning I can look through it to see a sphere I added there. Did you make other alterations to your code? I tried it out, but don't get the same behaviour like you have described. OK in your example I cannot look trough them, but changing the camera position, doesn't show the text because cullface is off by default I guess. – Eddy Alleman Aug 24 '19 at 11:12
  • @EddyAlleman Thanks for your comment, I added a screenshot of what I get and explained what rendering behaviour I am expecting. – vre Aug 24 '19 at 13:22
  • you're welcome. you mention "When looking at the scene from behind ( changing the position of the camera to Qt.vector3d(0,0,-40) ) all texts render OK." This is the part I don't understand. Do you really see the text from behind? I can only see the yellow sphere when I change the lines: position: Qt.vector3d(0,0,-40) instead of // position: Qt.vector3d( 0.0, 0.0, 40 ) Did you change back culling anywhere? – Eddy Alleman Aug 24 '19 at 13:32
  • @EddyAlleman , yeah I had a version where I could see through the text from the backside getting the correct drawing order but not from the front side. But I don't have access to my work environment so cannot reproduce it right now. – vre Aug 24 '19 at 13:43
  • ok, would be good to know the exact circumstances, which could trigger this behaviour. – Eddy Alleman Aug 24 '19 at 14:05

2 Answers2

4

The issue seems to be the line

zValue = vertexPosition.z;

in the vertex shader. vertexPosition is a coordinate in model space. If you want to calculate the the z distance to the camera then you've to transform the coordinate to view space by the modelView matrix:

vec4 viewPosition = modelView * vec4(vertexPosition.xyz, 1.0);
zValue = -viewPosition.z;

Note, since the view space z axis points out of the viewport, the coordinate has to be inverted to get the distance to the camera.


Another possibility, which seems even be more correct to me, is to calculate the clip space coordinate and further the normalized device coordinate. The clip space coordinate is calculated by the transformation by the model view matrix (mvp) and the normalized device coordinate, by Perspective divide:

vec4 clipPosition = modelView * vec4(vertexPosition.xyz, 1.0);
zValue = clipPosition.z / clipPosition.w;

The normalized device coordinates are in range [-1, 1]. The z component can linearly be mapped to the depth of the fragment. By default the depth range is [0, 1] (except it is changed by glDepthRange.
So in the fragment shader gl_FragDepth can be set by:

gl_FragDepth = zValue * 0.5 + 0.5;

If you use alpha Blending, then the Depth Test has to be disabled.

The objects behind other objects may not be drawn at all, because they are discarded by the depth test. Of course this depends on the drawing order. If the text which is covered is drawn first, then it will work. But if it is drawn last, then it is discarded. This explains the different behavior dependent on the direction of view.
Note, when blending is active, then the color doesn't affect the color buffer (if alpha is 0), but if the depth test is enable, then of course the depth is written to the depth buffer.

The only alternative is to draw the objects in sorted order form the back to the front. Of course the order depends on the direction of view, so the objects would have to be sorted per frame.

Rabbid76
  • 202,892
  • 27
  • 131
  • 174
  • Thanks for your detailed answer, appreciate that. I will need to test this more thoroughly with access to my work environment, as a first quick test did not reveal a change. – vre Aug 24 '19 at 13:25
  • 1
    @vre I just saw the image. I think you've a basic misunderstanding. If you use alpha [Blending](https://www.khronos.org/opengl/wiki/Blending), the the depth test has to be disabled! The objects behind are not drawn at all, because they are discarded by the depth test, if the object in front are drawn before – Rabbid76 Aug 24 '19 at 13:29
  • Thank you, I will try your suggestion and setup a different Qt3D framegraph with depth testing disabled. – vre Aug 24 '19 at 13:50
3

I did some tests and to get the unwanted behaviour, I made some changes to your code by messing up the order of the entities used. As you know the order of the entities is important. In my example the text2DEntity "textFront" is put before the text2DEntity "textBack" in the hierarchy. So without changing your render environment we get something like this: problem I added a red sphere to test a little deeper.

I found a solution using a Forward renderer and without working with a depth buffer. Here is the result (of course, I didn't change the order of the entities): solution

We have to use a SortPolicy, so that the forward renderer knows which object is in front of another. This will change the order of entities compared to the distance of the camera and not on the hierarchy order of the qml file.

components: [
    RenderSettings {
        activeFrameGraph: SortPolicy {
            sortTypes: [
                SortPolicy.BackToFront
            ]

            ForwardRenderer {
                camera: camera
                clearColor: "black"
            }
        }
    },
    // Event Source will be set by the Qt3DQuickWindow
    InputSettings { }
]

Here is the full file content for easy testing:

BrokenEntity.qml

import Qt3D.Core 2.12 
import Qt3D.Render 2.12
import Qt3D.Input 2.12
import Qt3D.Extras 2.13
import QtQuick 2.12 as QQ2

Entity {
    id: sceneRoot

    Camera {
        id: camera
        projectionType: CameraLens.PerspectiveProjection
        fieldOfView: 45
        nearPlane : 0.1
        farPlane : 1000.0
        position: Qt.vector3d( 0.0, 0.0, 40 )
        upVector: Qt.vector3d( 0.0, 1.0, 0.0 )
        viewCenter: Qt.vector3d( 0.0, 0.0, 0.0 )
    }
    OrbitCameraController { camera: camera }

        components: [
            RenderSettings {
                activeFrameGraph: SortPolicy {
                    sortTypes: [
                        SortPolicy.BackToFront
                    ]

                    ForwardRenderer {
                        camera: camera
                        clearColor: "black"
                    }
                }
            },
            // Event Source will be set by the Qt3DQuickWindow
            InputSettings { }
        ]

    Entity {
        id: textFront
        components: [ Transform { translation: Qt.vector3d(-12.5,-5,-20) } ] //IKKE

        Text2DEntity {
            font.family: "Sans Serif"
            font.pointSize: 3
            color: "white"
            text: "textFront"
            width: text.length * font.pointSize*2
            height: font.pointSize * 4
        }
    }

    PhongMaterial {
        id: material
        ambient: Qt.rgba(1, 1, 0, 1)
        diffuse: Qt.rgba(1, 1, 0, 1)
    }
    PhongMaterial {
        id: material2
        ambient: Qt.rgba(1, 0, 0, 1)
        diffuse: Qt.rgba(1, 0, 0, 1)
    }
    SphereMesh {
        id: sphereMesh
        radius: 5
        rings: 50
        slices: 50
    }

    Entity {
        id: mysphere
        Transform {
            id: sphereTransform
            translation: Qt.vector3d(0,0,-25)
            scale3D: Qt.vector3d(1, 1, 1)
        }
        components: [ sphereMesh, material, sphereTransform ]
    }

    Entity {
        id: mysphere2
        Transform {
            id: sphereTransform2
            translation: Qt.vector3d(0,0,-50)
            scale3D: Qt.vector3d(1, 1, 1)
        }
        components: [ sphereMesh, material2, sphereTransform2 ]
    }

    Entity {
        id: textBack
        components: [ Transform { translation: Qt.vector3d(-25,-5,-30) } ]

        Text2DEntity {
            font.family: "Sans Serif"
            font.pointSize: 10
            color: Qt.rgba(0, 1, 0, 1.0)
            text: "textBack"
            width: text.length * font.pointSize
            height: font.pointSize * 2
        }
    }
}
Eddy Alleman
  • 1,096
  • 2
  • 10
  • 21
  • 1
    Referencing my answer from [here](https://stackoverflow.com/questions/55001233/qt3d-draw-transparent-qspheremesh-over-triangles/55033342#55033342) `QSortPolicy` is not enough. You also need two framegraph branches, one for the opaque objects and one for the transparent ones and make the opaque branch get drawn first. – Florian Blume Aug 28 '19 at 07:25
  • 1
    Hi Florian, thanks for your helpfull comment/thorough answer in your link. There is a difference between your answer and the question asked here. Here we use text2DEntities, which have most likely a different distance from the camera, in contrast to a doughnut. I did some tests, with animation and once the sortpolicy changes (status1: text1 in front of text2 -> status2: text2 in front of text1) the text2Dentities maintain their transparancy around the tokens. My goals was to provide a solution as simple as possible. Have you tried the example? It is easy to play with and change the hierarchy. – Eddy Alleman Aug 28 '19 at 07:58
  • 1
    Ah sorry, my bad! I didn't realize that the text entities are 2D. If the simpler solution works why use a more complex one, you're right :) – Florian Blume Aug 28 '19 at 08:16
  • @FlorianBlume As my scene is more complex than shown, I think I will be in need of two framegraph branches. Thanks for mentioning it. – vre Aug 29 '19 at 06:22