Qt Quick 3D - 画面空間の反射の例

Qt Quick 3D での反射の例です。

この例では、モデル上でScreen Space Reflections(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

©2024 The Qt Company Ltd. 本書に含まれるドキュメントの著作権は、それぞれの所有者に帰属します。 本書で提供されるドキュメントは、Free Software Foundation が発行したGNU Free Documentation License version 1.3に基づいてライセンスされています。 Qtおよびそれぞれのロゴは、フィンランドおよびその他の国におけるThe Qt Company Ltd.の 商標です。その他すべての商標は、それぞれの所有者に帰属します。