Qt Quick 3D - スクリーン空間での反射の例

Qt Quick 3Dでの反射のデモ。

この例では、モデル上でスクリーン空間反射(SSR)を使用して反射を作成する方法を示します。SSRは、シーンに反射を追加することによってシーンを向上させることができるポストプロセッシングエフェクトです。SSRの背景にあるアイデアは、オブジェクトがレンダリングされた後に、スクリーンスペースで反射を計算できるということです。各フラグメントについて、カメラからこのフラグメントにレイが照射され、フラグメントの法線を中心に反射されます。その後、反射されたレイをたどり、オブジェクトに当たるかどうかを判断します。オブジェクトに当たった場合、フラグメントはそのオブジェクトを反射します。SSRが失敗する状況もあります。例えば、反射光線がカメラの後ろにあるオブジェクトに当たった場合です。反射光はオブジェクトがレンダリングされた後にスクリーン空間で計算されるため、カメラの後ろにあるオブジェクトの色に関する情報は得られません。SSR にはいくつかの欠点がありますが、シーンによりリアルさを加えます。

この例では、Model に使用できるCustom Materials を使って 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;
}

プロジェクト例 @ code.qt.io

© 2025 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.