Skip to main content

Qt 6.12のSkyMaterial: 表現の限界を超える空を描く

コメント
このブログは「With Qt 6.12, the SkyMaterial is the limit」の抄訳です。

Qt Quick 3D 6.12では、新しいSkyMaterial QML型が追加されました。本記事では、その概要、使い方、そしてどのような場面で役立つのかを解説します。

SkyMaterialを使うと、シーンの空を描画するカスタムのフラグメントシェーダーを記述できます。さらに、そこから画像ベースのライティング(IBL)を計算することもできます。つまり、同じ空が見える背景として機能すると同時に、シーンの光源としても機能します。これにより、移動する太陽、昼夜のサイクル、雲、星などを備えたダイナミックな空を作成できます。

SkyMaterialに設定したQMLプロパティは、カスタムシェーダー内で自動的に利用できるようになります。プロパティが更新されるたびにSkyMaterialは再描画されるため、ダイナミックな空が実現できます。描画はGPU上で行われるため、パフォーマンスへの影響はシェーダーの複雑さと更新頻度によって決まります。品質やパフォーマンスに関する一部のプロパティを調整して、パフォーマンスの予算に合わせることも可能です。

3Dシーンで SkyMaterial を使うには、SceneEnvironment::skyMaterialプロパティを SkyMaterial オブジェクトに設定し、SceneEnvironment::backgroundModeをSceneEnvironment.SkyMaterialに設定します。

最初の空


SkyMaterialの最小限の例を見てみましょう:

import QtQuick
import QtQuick3D

Window {
    width: 800
    height: 600
    visible: true

    View3D {
        anchors.fill: parent
        environment: SceneEnvironment {
            backgroundMode: SceneEnvironment.SkyMaterial
            skyMaterial: SkyMaterial {
                fragmentShaderCode: `
                    void MAIN()
                    {
                        vec3 dir = normalize(qt_eyeDir);
                        // Map from [-1, 1] to [0, 1]
                        FRAGCOLOR = vec4(dir * 0.5 + 0.5, 1.0);
                    }
                `
            }
        }

        camera: PerspectiveCamera {
            position: Qt.vector3d(0, 100, 400)
        }

        Model {
            source: "#Cube"
            materials: PrincipledMaterial {}
        }
    }
}


実行結果は次のとおりです:

視線方向で色付けされた空を背景にした立方体

この例では、中央に立方体がある3Dシーンと、fragmentShaderCodeにインラインのカスタムフラグメントシェーダーを設定したSkyMaterialがあります(ファイルからシェーダーを読み込む場合はfragmentShaderを使用します)。skyboxModeプロパティは既定でSkyMaterial.Cubemapに設定されています。これにより、空はキューブマップにレンダリングされ、6つの面それぞれでシェーダーが実行されます。同様に、enableIBLプロパティも既定でtrueに設定されています。これにより、キューブマップは放射輝度マップと照度マップにフィルタリングされ、IBLに使用されます。照度マップが立方体を照らすため、各面は自分が向いている空の部分の色を帯びることになります。

シェーダーを見てみましょう。まず、MAIN()エントリーポイントを定義する必要があります。これは、キューブマップのレンダリング中に生成される各フラグメントに対して実行されます。シェーダーは組み込み変数qt_eyeDirにアクセスできます。これは、キューブマップの中心から描画中のフラグメントへ向かう、正規化されていない方向ベクトルを提供します。最終的なピクセルの色は、フラグメントシェーダーの出力変数であるFRAGCOLORに書き込む必要があります。

上の例では、サンプリングされた方向からFRAGCOLORのRGB成分を計算しています。これは、シェーダーが正しく動作していることを確認するためです。

プロパティがユニフォームになる

先ほど述べたように、SkyMaterialに設定された互換性のあるプロパティは、シェーダー内で自動的に利用できます。そこで、プロパティを追加し、それに反応するようにシェーダーを調整してみましょう:

skyMaterial: SkyMaterial {
    property real angle: 0
    NumberAnimation on angle {
        from: 0
        to: 360
        duration: 6000
        loops: Animation.Infinite
    }

    fragmentShaderCode: `
        void MAIN()
        {
            vec3 dir = normalize(qt_eyeDir);
            // Rotate the view direction around the Y axis by angle degrees.
            float a = radians(angle);
            float c = cos(a);
            float s = sin(a);
            vec3 rotated = vec3(c * dir.x + s * dir.z,
                                dir.y,
                                -s * dir.x + c * dir.z);
            vec3 color = rotated * 0.5 + 0.5;
            FRAGCOLOR = vec4(color, 1.0);
        }
    `
}


ここでは、real型のプロパティangleと、それを操作するNumberAnimationを追加しています。これにより、視線方向がY軸を中心にangle度回転し、空が6秒ごとに一周するようになります。実行結果は次のとおりです:



angleが変化するたびに、空とIBLのキューブマップが再レンダリングされることに注意してください。キューブマップの解像度が高いほど処理に時間がかかるため、radianceMapSizeプロパティでサイズを調整すると効果的です。

ここのように空が毎フレーム変化し、IBLが不要な場合は、enableIBLをfalseに、skyboxModeをSkyMaterial.ScreenSpaceFull、SkyMaterial.ScreenSpaceHalf、SkyMaterial.ScreenSpaceQuarterのいずれかに設定できます。こうすると、キューブマップもIBLも描画されず、空のシェーダーが画面上で直接実行されます。解像度はそれぞれ全体、半分、4分の1です。ただし、これは変化がない場合でも毎フレーム実行されるため、ほぼ静的な空であれば、既定のSkyMaterial.Cubemapモードの方が低コストです。このモードでは、一度キューブマップにレンダリングされたものをサンプリングするだけで済みます。

real型以外にも、SkyMaterialはint、bool、color、ベクトル、行列、テクスチャなど、GLSLに対応する型をサポートしています。対応する型とGLSLでの対応関係の全一覧は、SkyMaterialのドキュメントをご覧ください。

プロシージャルな空

こうした種類のプロパティを使えば、より高度なシェーダーを実装できます。では、実際にやってみましょう。新しいSkyMaterialオブジェクトは次のようになります:

skyMaterial: SkyMaterial {
    iblSampleCount: 256

    property color skyTopColor: "#A5D6F1"
    property color skyHorizonColor: "#D6EAFA"
    property real skyCurve: 0.09
    property real skyEnergy: 1.0
    property color groundBottomColor: "#282F36"
    property color groundHorizonColor: "#6C655F"
    property real groundCurve: 0.02
    property real groundEnergy: 1.0
    property color sunColor: "#ffffff"
    property real sunLatitude: 35.0
    property real sunLongitude: 0.0
    property real sunAngleMin: 1.0
    property real sunAngleMax: 100.0
    property real sunCurve: 0.05
    property real sunEnergy: 1.0

    readonly property vector3d sunDirection: {
        const latRad = sunLatitude * Math.PI / 180.0
        const lonRad = sunLongitude * Math.PI / 180.0
        return Qt.vector3d(
            -Math.cos(lonRad) * Math.cos(latRad),
             Math.sin(latRad),
            -Math.sin(lonRad) * Math.cos(latRad)
        )
    }

    fragmentShaderCode: `
        const float PI = 3.14159265359;
        const float HALF_PI = PI * 0.5;

        float ease(float x, float c)
        {
            x = clamp(x, 0.0, 1.0);
            if (c > 0.0) {
                if (c < 1.0)
                    return 1.0 - pow(1.0 - x, 1.0 / c);
                return pow(x, c);
            }
            if (c < 0.0) {
                if (x < 0.5)
                    return pow(x * 2.0, -c) * 0.5;
                return (1.0 - pow(1.0 - (x - 0.5) * 2.0, -c)) * 0.5 + 0.5;
            }
            return 0.0;
        }

        void MAIN()
        {
            vec3 direction = normalize(qt_eyeDir);
            float vAngle = acos(clamp(direction.y, -1.0, 1.0));

            vec3 color;

            if (direction.y < 0.0) {
                float t = ease((vAngle - HALF_PI) / HALF_PI, groundCurve);
                color = mix(groundHorizonColor.rgb, groundBottomColor.rgb, t) * groundEnergy;
            } else {
                float t = ease(1.0 - vAngle / HALF_PI, skyCurve);
                color = mix(skyHorizonColor.rgb, skyTopColor.rgb, t) * skyEnergy;

                float sunAngle = degrees(acos(clamp(dot(normalize(sunDirection), direction), -1.0, 1.0)));
                vec3 sunRadiance = sunColor.rgb * sunEnergy;

                if (sunAngle < sunAngleMin) {
                    color = sunRadiance;
                } else if (sunAngle < sunAngleMax) {
                    float tSun = ease(
                        (sunAngle - sunAngleMin) / (sunAngleMax - sunAngleMin),
                        sunCurve
                    );
                    color = mix(sunRadiance, color, tSun);
                }
            }

            FRAGCOLOR = vec4(color, 1.0);
        }
    `
}


シーンは次のようになります:

プロシージャルな空と太陽を背景にした立方体

上部にあるプロパティは、空と地面の色、そして太陽の位置を制御します。シェーダーはこれらすべてにアクセスでき、いずれかの値が変更されるたびに空が再描画されます。iblSampleCountプロパティは、IBLの計算で使用するサンプル数を制御し、ひいては生成される放射輝度マップの視覚的な品質を左右します。ここでは空が静的で、一度だけ計算されるため、この値を大きくして視覚的な品質を高めることができます。

実際に試してみる

SkyMaterialを基盤にすると、さらに多くのことが実現できます。その一例として、Qt 6.12に付属する公式のQt Quick 3D - Sky Material Exampleをご覧ください。この例では、レイマーチングで描画された雲とともに、物理ベースの大気散乱を実装しています。実行結果は次のとおりです:


Qt 6.12のSkyMaterialの紹介は以上です。この記事が皆さまの独自の空を作るきっかけになれば幸いです。どのような作品が生まれるのか、楽しみにしています。

トピック

コメント

ブログを購読

Qt 6.12 を
今すぐ試そう!

最新リリースはこちらからダウンロードできます。 www.qt.io/download

Qt 6.12 LTS がリリースされました!アプリケーション開発者やデバイス開発者向けに、多くの新機能と改善が追加されています。 

採用情報 

現在、さまざまなポジションで採用を行っています。募集職種はこちら をご覧ください。また、Instagram をフォローして #QtPeople の働き方もぜひチェックしてください。