Qt Quick 3D - 屏幕空间反射示例

演示了 Qt Quick 3D 中的反射。

此示例演示了如何使用 屏幕空间反射(SSR)在模型上创建反射。SSR 是一种后处理效果,可以通过向场景添加反射来增强场景。SSR 的想法是,在对象渲染后,可以在屏幕空间中计算反射。对于每个片段,从摄像机向该片段发射一束光线,然后围绕片段的法线进行反射。之后,我们沿着反射光线追踪,并确定它是否会撞击对象。如果撞击了对象,则片段将反射该对象。SSR 会在某些情况下失败。例如,当反射光线击中摄像机后面的对象时。由于反射是在对象渲染后的屏幕空间中计算的,因此没有关于摄像机后面对象颜色的信息。尽管 SSR 有一些缺点,但它为场景添加了更多的真实感。

此示例使用 自定义材质 实现了 SSR,这些材质可用于 模型,使得模型能够反射其周围环境。

Model {
    source: "#Rectangle"
    scale: Qt.vector3d(5, 5, 5)
    eulerRotation.x: -90
    eulerRotation.z: 180
    position: Qt.vector3d(0.0, -50.0, 0.0)
    materials: ScreenSpaceReflections {
        depthBias: depthBiasSlider.value
        rayMaxDistance: distanceSlider.value
        marchSteps: marchSlider.value
        refinementSteps: refinementStepsSlider.value
        specular: specularSlider.value
        materialColor: materialColorCheckBox.checked ? "transparent" : "dimgray"
    }
}

场景中的其他一些对象是静态的或者旋转在表面上以显示反射。

Node {

    Model {
        source: "#Cube"
        eulerRotation.y: 0
        scale: Qt.vector3d(1, 1, 1)
        position: Qt.vector3d(50.0, 40.0, 50.0)
        materials:  DefaultMaterial {
            diffuseMap: Texture {
                source: "qt_logo_rect.png"
            }
        }
    }

    Node{

        Model {
            source: "#Sphere"
            position: Qt.vector3d(-400.0, screenSpaceReflectionsView.modelHeight, 0.0)
            materials: DefaultMaterial {
                diffuseColor: "magenta"
            }
        }
    }

    Node{
        eulerRotation.y: screenSpaceReflectionsView.modelRotation
        position.y: screenSpaceReflectionsView.modelHeight

        Model {
            source: "#Sphere"
            pivot: Qt.vector3d(0, 0.0, 0.0)
            position: Qt.vector3d(200.0, 0.0, 0.0)
            materials: DefaultMaterial {
                diffuseColor: "green"
            }
        }
    }

    Node{
        eulerRotation.y: screenSpaceReflectionsView.modelRotation
        position.y: screenSpaceReflectionsView.modelHeight

        Model {
            source: "#Sphere"
            eulerRotation.y: 45
            position: Qt.vector3d(0.0, 0.0, -200.0)
            materials: DefaultMaterial {
                diffuseColor: "blue"
            }
        }
    }

    Node{
        eulerRotation.y: screenSpaceReflectionsView.modelRotation
        position.y: screenSpaceReflectionsView.modelHeight

        Model {
            source: "#Sphere"
            position: Qt.vector3d(0.0, 0.0, 200.0)
            materials: DefaultMaterial {
                diffuseColor: "red"
            }
        }
    }

着色器代码

在深入研究着色器代码之前,让我们检查一些可以用来控制反射的参数。

depthBias此参数用于检查光线深度和对象深度之间的差异是否在某个阈值范围内。
rayMaxDistance控制光线在视图空间中的结束点距离。
marchSteps控制用于计算的步数。增加步数会减少每个迭代中光线移动的片段数,从而提高质量。
refinementSteps在找到反射光线击中对象的位置后,进行细化过程,尝试找到击中的确切位置。此参数控制应使用多少步骤。当 marchSteps 较小时,它可以提供更好的结果。
specular0 到 1 之间的值,用于控制模型具有多少反射率。
materialColor为模型提供颜色。此颜色与反射颜色混合。

着色器从摄像机到片段的方向开始,然后围绕片段的法线进行反射。光线起止点在视图空间中计算,然后将其转换为屏幕空间。在屏幕空间中追踪反射光线的好处是它会产生更好的质量。此外,光线可能在视图空间中覆盖很大的距离,但在屏幕空间中可能只有少数片段。

计算从起始片段到终止片段的向量,并除以marchSteps

之后调用rayMarch函数。它在屏幕空间中每一步都移动射线,并将其转换回视图空间。它还使用场景的DEPTH_TEXTURE获取此片段的对象。计算射线和对象的深度之间的差异,并将其与depthBias比较。如果找到碰撞点,则调用refinementStep函数。

void rayMarch(vec2 rayStepVector, vec2 size)
{
    for(int i = 0; i < marchSteps; i++)
    {
        rayData.rayFragCurr += rayStepVector;
        rayData.rayCoveredPart = length(rayData.rayFragCurr - rayData.rayFragStart) / length(rayData.rayFragEnd - rayData.rayFragStart);
        rayData.rayCoveredPart = clamp(rayData.rayCoveredPart, 0.0, 1.0);
        float rayDepth = rayViewDepthFromScreen(size);
        rayData.objHitViewPos = viewPosFromScreen(rayData.rayFragCurr, size);
        float deltaDepth = rayDepth - rayData.objHitViewPos.z;

        if(deltaDepth > 0 && deltaDepth < depthBias)
        {
            rayData.hit = 1;
            refinementStep(rayStepVector, size);
            return;
        }
    }
}

细化步骤与rayMarch相同,除了它尝试找到碰撞发生的确切位置,因此每次迭代都前进射线的步骤的一半距离。

void refinementStep(vec2 rayStepVector, vec2 size)
{
    for(int i = 0; i < refinementSteps; i++)
    {
        rayData.rayCoveredPart = length(rayData.rayFragCurr - rayData.rayFragStart) / length(rayData.rayFragEnd - rayData.rayFragStart);
        rayData.rayCoveredPart = clamp(rayData.rayCoveredPart, 0.0, 1.0);
        float rayDepth = rayViewDepthFromScreen(size);
        rayData.objHitViewPos = viewPosFromScreen(rayData.rayFragCurr, size);
        float deltaDepth = rayDepth - rayData.objHitViewPos.z;

        rayStepVector *= 0.5;
        if(deltaDepth > 0 && deltaDepth < depthBias)
            rayData.rayFragCurr -= rayStepVector;
        else
            rayData.rayFragCurr += rayStepVector;
    }
}

将反射的可见性设置为碰撞值,然后进行一些可见性检查。如前所述,如果反射的射线击中相机后面的物体,SSR将会失败,因此根据反射射线指向相机的程度,逐渐减弱可见性。可见性也会根据击中物体从射线起始点的距离减弱。

float visibility = rayData.hit;
/* Check if the ray hit an object behind the camera. This means information about the object can not be obtained from SCREEN_TEXTURE.
   Start fading the visibility according to how much the reflected ray is moving toward the opposite direction of the camera */
visibility *= (1 - max(dot(-normalize(fragViewPos), reflected), 0));

/* Fade out visibility according how far is the hit object from the fragment */
visibility *= (1 - clamp(length(rayData.objHitViewPos - rayData.rayViewStart) / rayMaxDistance, 0, 1));
visibility = clamp(visibility, 0, 1);

最后,从SCREEN_TEXTURE计算反射颜色并将其与材质颜色混合。

vec2 uv = rayData.rayFragCurr / size;
uv = correctTextureCoordinates(uv);
vec3 reflectionColor = texture(SCREEN_TEXTURE, uv).rgb;
reflectionColor *= specular;

vec3 mixedColor = mix(materialColor.rgb, reflectionColor, visibility);
BASE_COLOR = vec4(mixedColor, materialColor.a);

辅助函数

在着色器代码中使用了一些辅助函数。correctTextureCoordinates函数根据所使用的图形API修复纹理坐标。在D3D11或Metal的情况下必须这样做。有关详细信息,请参阅CustomMaterial文档。

vec2 correctTextureCoordinates(vec2 uv)
{
    if(FRAMEBUFFER_Y_UP < 0 && NDC_Y_UP == 1)
        uv.y = 1 - uv.y;

    return uv;
}

rayFragOutOfBound函数检查射线在移动后是否在屏幕之外。在这种情况下,不使用反射颜色,因为没有关于屏幕之外任何事物的信息。

bool rayFragOutOfBound(vec2 rayFrag, vec2 size)
{
    if(rayFrag.x > size.x || rayFrag.y > size.y)
        return true;

    if(rayFrag.x < 0 || rayFrag.y < 0)
        return true;

    return false;
}

viewPosFromScreen函数通过使用DEPTH_TEXTURE获取对象在视图空间中的位置。

vec3 viewPosFromScreen(vec2 fragPos, vec2 size)
{
    vec2 uv = fragPos / size;
    vec2 texuv = correctTextureCoordinates(uv);

    float depth = textureLod(DEPTH_TEXTURE, texuv, 0).r;
    if(NEAR_CLIP_VALUE  < 0.0)
        depth = 2 * depth - 1.0;

    vec3 ndc = vec3(2 * uv - 1, depth);
    vec4 viewPos = INVERSE_PROJECTION_MATRIX * vec4(ndc, 1.0);
    viewPos /= viewPos.w;
    return viewPos.xyz;
}

rayViewDepthFromScreen函数获取射线在视图空间中的当前位置。这次,通过线性插值射线的起始点深度和终止点深度之间的值来获取深度值。

float rayViewDepthFromScreen(vec2 size)
{
    vec2 uv = rayData.rayFragCurr / size;
    float depth = mix(rayData.rayFragStartDepth, rayData.rayFragEndDepth, rayData.rayCoveredPart);
    vec3 ndc = vec3(2 * uv - 1, depth);
    vec4 viewPos = INVERSE_PROJECTION_MATRIX * vec4(ndc, 1.0);
    viewPos /= viewPos.w;
    return viewPos.z;
}

文件

图片

© 2024 The Qt Company Ltd. 本文档中包含的贡献的版权归其各自的所有者所有。本文档是根据自由软件开发基金会发布的GNU自由文档许可证版本1.3的条款许可的。Qt及相关的标志是The Qt Company Ltd在芬兰及/或其他国家/地区注册的商标。所有其他商标均为其各自所有者的财产。