Qt Quick 3D glTF 에셋을 사용한 소개

Qt Quick 3D - 소개 예제에서는 Qt Quick 3D 을 사용하여 QML 기반 애플리케이션을 만드는 방법을 간략하게 소개하지만, 구와 원통과 같은 기본 제공 프리미티브만 사용합니다. 이 페이지에서는 크로노스 glTF 샘플 모델 저장소의 일부 모델을 사용하여 glTF 2.0 에셋을 사용한 소개를 제공합니다.

스켈레톤 애플리케이션

다음 애플리케이션부터 시작하겠습니다. 이 코드 스니펫은 qml 명령줄 툴을 사용하여 그대로 실행할 수 있습니다. 결과는 아무것도 없는 매우 녹색의 3D 뷰입니다.

import QtQuick
import QtQuick3D
import QtQuick3D.Helpers

Item {
    width: 1280
    height: 720

    View3D {
        anchors.fill: parent

        environment: SceneEnvironment {
            backgroundMode: SceneEnvironment.Color
            clearColor: "green"
        }

        PerspectiveCamera {
            id: camera
        }

        WasdController {
            controlledObject: camera
        }
    }
}

에셋 임포트하기

샘플 모델 리포지토리에 있는 두 개의 glTF 2.0 모델을 사용하겠습니다: Sponza와 Suzanne입니다.

이러한 모델에는 일반적으로 .gltf 파일 외에 별도의 바이너리 파일에 저장된 여러 텍스처 맵과 메시(지오메트리) 데이터가 함께 제공됩니다:

이 모든 것을 Qt Quick 3D 씬에 어떻게 가져올 수 있을까요?

여러 가지 옵션이 있습니다:

  • 씬에서 인스턴스화할 수 있는 QML 컴포넌트를 생성합니다. 이 변환을 수행하는 명령줄 도구는 Balsam 툴입니다. 이 툴은 사실상 서브 씬인 .qml 파일을 생성할 뿐만 아니라 메시(지오메트리) 데이터를 최적화된 빠른 로드 형식으로 다시 패킹하고 텍스처 맵 이미지 파일도 복사합니다.
  • Balsam용 GUI 프런트엔드인 balsamui 를 사용하여 동일한 작업을 수행합니다.
  • 를 사용하는 경우 Qt Design Studio를 사용하면 에셋 가져오기 프로세스가 시각 디자인 도구에 통합됩니다. 예를 들어 .gltf 파일을 해당 패널에 끌어다 놓는 방식으로 임포트를 트리거할 수 있습니다.
  • 특히 glTF 2.0 에셋의 경우 런타임 옵션인 RuntimeLoader 유형도 있습니다. 이 옵션을 사용하면 Balsam과 같은 툴을 통한 사전 처리 없이 런타임에 .gltf 파일(및 관련 바이너리 및 텍스처 데이터 파일)을 로드할 수 있습니다. 이는 사용자가 제공한 에셋을 열고 로드하려는 애플리케이션에서 매우 유용합니다. 반면에 이 접근 방식은 성능 측면에서 효율성이 현저히 떨어집니다. 따라서 이 소개에서는 이 접근 방식에 초점을 맞추지 않겠습니다. 이 접근 방식의 예는 Qt Quick 3D - 런타임로더 예시를 참조하세요.

balsambalsamui 애플리케이션은 모두 Qt와 함께 제공되며, Qt Quick 3D 이 설치 또는 빌드되어 있다고 가정할 때 다른 유사한 실행 도구와 함께 디렉토리에 있어야 합니다. 대부분의 경우 추가 인수를 지정할 필요 없이 .gltf 파일의 명령줄에서 balsam을 실행하는 것으로 충분합니다. 하지만 balsamui 또는 Qt Design Studio 옵션을 사용하는 경우 명령줄 또는 대화형 옵션이 많다는 점에 유의할 필요가 있습니다. 예를 들어 베이크된 라이트맵으로 정적 전역 조명을 제공하는 작업을 할 때는 런타임에 이 잠재적으로 소모적인 프로세스를 수행하는 대신 --generateLightmapUV 을 전달하여 에셋 임포트 시 생성된 추가 라이트맵 UV 채널을 가져와야 할 수 있습니다. 마찬가지로 씬에서 자동 LOD를 활성화하기 위해 단순화된 버전의 메시를 생성해야 하는 경우 --generateMeshLevelsOfDetail 가 필수적입니다. 다른 옵션을 사용하면 누락된 데이터(예: --generateNormals)를 생성하고 다양한 최적화를 수행할 수 있습니다.

balsamui 에서 명령줄 옵션은 인터랙티브 요소에 매핑됩니다:

발삼을 통해 임포트하기

시작해 봅시다! https://github.com/KhronosGroup/glTF-Sample-Models git 리포지토리가 어딘가에 체크 아웃되어 있다고 가정하면 예제 애플리케이션 디렉토리에서 .gltf 파일의 절대 경로를 지정하여 balsam을 간단히 실행할 수 있습니다:

balsam c:\work\glTF-Sample-Models\2.0\Sponza\glTF\Sponza.gltf

이렇게 하면 Sponza.qml, meshes 하위 디렉터리 아래에 .mesh 파일이 있고 maps 아래에 텍스처 맵이 복사됩니다.

참고: 이 qml 파일은 단독으로 실행할 수 없습니다. 이 파일은 컴포넌트이며, View3D 와 연결된 3D 씬 내에서 인스턴스화되어야 합니다.

여기서 프로젝트 구조는 매우 간단합니다. 에셋 qml 파일이 메인 .qml 씬 바로 옆에 있기 때문입니다. 따라서 표준 QML 컴포넌트 시스템을 사용하여 Sponza 유형을 간단히 인스턴스화할 수 있습니다. (런타임에 파일 시스템에서 Sponza.qml을 찾습니다).

그러나 모델(서브씬)을 추가하는 것만으로는 의미가 없습니다. 기본적으로 머티리얼은 전체 PBR 조명 계산을 수행하므로 DirectionalLight, PointLight 또는 SpotLight 와 같은 라이트가 없거나 the environment 를 통해 이미지 기반 조명을 활성화하지 않으면 씬에서 아무것도 표시되지 않기 때문입니다.

지금은 기본 설정으로 DirectionalLight 을 추가하기로 선택합니다. (즉, 색상은 white 이며 빛은 Z 축 방향으로 방출됨).

import QtQuick
import QtQuick3D
import QtQuick3D.Helpers

Item {
    width: 1280
    height: 720

    View3D {
        anchors.fill: parent

        environment: SceneEnvironment {
            backgroundMode: SceneEnvironment.Color
            clearColor: "green"
        }

        PerspectiveCamera {
            id: camera
        }

        DirectionalLight {
        }

        Sponza {
        }

        WasdController {
            controlledObject: camera
        }
    }
}

qml 도구로 실행하면 로드 및 실행되지만, Sponza 모델이 카메라 뒤에 있기 때문에 기본적으로 장면이 모두 비어 있습니다. 스케일도 이상적이지 않습니다. 예를 들어 WASD 키와 마우스( WasdController)로 이동하는 것이 제대로 느껴지지 않습니다.

이 문제를 해결하기 위해 스폰자 모델(서브씬)의 스케일을 X, Y, Z 축을 따라 100 으로 조정했습니다. 또한 카메라의 시작 Y 위치가 100으로 상향 조정됩니다.

import QtQuick
import QtQuick3D
import QtQuick3D.Helpers

Item {
    width: 1280
    height: 720

    View3D {
        anchors.fill: parent

        environment: SceneEnvironment {
            backgroundMode: SceneEnvironment.Color
            clearColor: "green"
        }

        PerspectiveCamera {
            id: camera
            y: 100
        }

        DirectionalLight {
        }

        Sponza {
            scale: Qt.vector3d(100, 100, 100)
        }

        WasdController {
            controlledObject: camera
        }
    }
}

이를 실행하면 다음과 같은 결과가 나옵니다:

마우스와 WASDRF 키를 사용하여 이동할 수 있습니다:

참고: 위에서 '모델'의 대안으로 subscene 을 여러 번 언급했습니다. 왜 그럴까요? glTF 형식에서는 103개의 서브메시가 있는 단일 모델이지만, materials list 에 103개의 요소가 있는 단일 Model 오브젝트에 매핑되는 Sponza 에셋에서는 명확하지 않지만, 에셋에는 각각 여러 서브메시 및 관련 머티리얼이 있는 models 이 얼마든지 포함될 수 있습니다. 이러한 모델은 부모-자식 관계를 형성할 수 있으며 추가 nodes 와 결합하여 이동, 회전 또는 크기 조정과 같은 변환을 수행할 수 있습니다. 따라서 렌더링된 결과가 시각적으로 단일 모델로 인식되더라도 임포트된 에셋을 완전한 서브씬, 즉 nodes 의 임의 트리로 보는 것이 더 적절합니다. 생성된 Sponza.qml 또는 해당 에셋에서 생성된 다른 QML 파일을 일반 텍스트 편집기에서 열어 구조를 파악합니다(물론 소스 에셋(이 경우 glTF 파일)의 디자인 방식에 따라 달라질 수 있음).

발사무이를 통해 임포트하기

두 번째 모델에서는 balsam 의 그래픽 사용자 인터페이스를 사용해 보겠습니다.

balsamui 을 실행하면 도구가 열립니다:

Suzanne 모델을 가져와 보겠습니다. 이 모델은 텍스처 맵이 두 개 있는 더 간단한 모델입니다.

추가 구성 옵션이 필요하지 않으므로 변환만 하면 됩니다. 결과는 특정 출력 디렉터리에 생성된 Suzanne.qml 및 몇 가지 추가 파일을 balsam: 실행하는 것과 동일합니다.

이 시점부터 생성된 에셋으로 작업하는 것은 이전 섹션과 동일합니다.

import QtQuick
import QtQuick3D
import QtQuick3D.Helpers

Item {
    width: 1280
    height: 720

    View3D {
        anchors.fill: parent

        environment: SceneEnvironment {
            backgroundMode: SceneEnvironment.Color
            clearColor: "green"
        }

        PerspectiveCamera {
            id: camera
            y: 100
        }

        DirectionalLight {
        }

        Sponza {
            scale: Qt.vector3d(100, 100, 100)
        }

        Suzanne {
            y: 100
            scale: Qt.vector3d(50, 50, 50)
            eulerRotation.y: -90
        }

        WasdController {
            controlledObject: camera
        }
    }
}

다시 말하지만, 인스턴스화된 수잔 노드에 스케일이 적용되고 모델이 스폰자 건물의 바닥에 닿지 않도록 Y 위치가 약간 변경됩니다.

Qt Quick 에서와 마찬가지로 모든 프로퍼티를 변경하고, 바인딩하고, 애니메이션을 적용할 수 있습니다. 예를 들어 Suzanne 모델에 연속 회전을 적용해 보겠습니다:

Suzanne {
    y: 100
    scale: Qt.vector3d(50, 50, 50)
    NumberAnimation on eulerRotation.y {
        from: 0
        to: 360
        duration: 3000
        loops: Animation.Infinite
    }
}

더 보기 좋게 만들기

더 많은 빛

이제 장면이 약간 어둡습니다. 다른 조명을 추가해 보겠습니다. 이번에는 그림자를 드리우는 PointLight 입니다.

import QtQuick
import QtQuick3D
import QtQuick3D.Helpers

Item {
    width: 1280
    height: 720

    View3D {
        anchors.fill: parent

        environment: SceneEnvironment {
            backgroundMode: SceneEnvironment.Color
            clearColor: "green"
        }

        PerspectiveCamera {
            id: camera
            y: 100
        }

        DirectionalLight {
        }

        Sponza {
            scale: Qt.vector3d(100, 100, 100)
        }

        PointLight {
            y: 200
            color: "#d9c62b"
            brightness: 5
            castsShadow: true
            shadowFactor: 75
        }

        Suzanne {
            y: 100
            scale: Qt.vector3d(50, 50, 50)
            NumberAnimation on eulerRotation.y {
                from: 0
                to: 360
                duration: 3000
                loops: Animation.Infinite
            }
        }

        WasdController {
            controlledObject: camera
        }
    }
}

이 장면을 실행하고 카메라를 조금 움직여 보니 이전보다 실제로 더 좋아 보이기 시작합니다:

조명 디버깅

PointLight 은 수잔 모델보다 약간 위에 배치되어 있습니다. Qt Design Studio 과 같은 시각적 도구를 사용하여 씬을 디자인할 때는 당연하지만, 디자인 도구 없이 개발할 때는 lights 및 기타 nodes 의 위치를 빠르게 시각화할 수 있으면 편리할 수 있습니다.

PointLight 에 자식 노드인 Model 를 추가하면 됩니다. 자식 노드의 위치는 부모에 상대적이므로 이 경우 기본값 (0, 0, 0) 이 사실상 PointLight 의 위치가 됩니다. 이 시스템에는 오클루전 개념이 없으므로 빛이 '벽'을 통과하는 데 문제가 없으므로 특정 지오메트리(이 경우 내장된 큐브) 내에 빛을 둘러싸는 것은 표준 실시간 조명 계산에 문제가 되지 않습니다. 레이트레이싱을 사용하여 조명을 계산하는 사전 베이크된 라이트맵을 사용한다면 이야기가 달라집니다. 이 경우 디버그 큐브를 조명보다 약간 위로 이동하여 큐브가 빛을 가리지 않도록 해야 합니다.

PointLight {
    y: 200
    color: "#d9c62b"
    brightness: 5
    castsShadow: true
    shadowFactor: 75
    Model {
        source: "#Cube"
        scale: Qt.vector3d(0.01, 0.01, 0.01)
        materials: PrincipledMaterial {
            lighting: PrincipledMaterial.NoLighting
        }
    }
}

여기서 사용하는 또 다른 트릭은 큐브에 사용된 머티리얼의 조명을 끄는 것입니다. 조명의 영향을 받지 않고 기본 기본 색상(흰색)을 사용하여 나타납니다. 디버깅 및 시각화 목적으로 사용되는 오브젝트에 유용합니다.

그 결과 작은 흰색 큐브가 나타나 PointLight 의 위치를 시각화하는 것을 확인할 수 있습니다:

스카이박스 및 이미지 기반 조명

또 다른 분명한 개선 사항은 배경에 대해 뭔가를 하는 것입니다. 녹색의 선명한 색상은 그다지 이상적이지 않습니다. 조명에도 영향을 주는 환경은 어떨까요?

적절한 HDRI 파노라마 이미지를 사용할 수 없으므로 절차적으로 생성된 하이 다이내믹 레인지 하늘 이미지를 사용해 보겠습니다. 이는 파일 기반이 아닌 동적으로 생성된 이미지 데이터를 지원하는 ProceduralSkyTextureDataTexture 의 도움을 받으면 쉽게 할 수 있습니다. source 을 지정하는 대신 textureData 속성을 사용합니다.

environment: SceneEnvironment {
    backgroundMode: SceneEnvironment.SkyBox
    lightProbe: Texture {
        textureData: ProceduralSkyTextureData {
        }
    }
}

참고: 예제 코드에서는 객체를 인라인으로 정의하는 것을 선호합니다. 이는 필수는 아니며, SceneEnvironment 또는 ProceduralSkyTextureData 객체를 객체 트리의 다른 곳에 정의한 다음 id 에서 참조할 수도 있습니다.

그 결과 스카이박스와 향상된 조명이 모두 생겼습니다. (전자는 backgroundMode 이 스카이박스로 설정되고 light probe 이 유효한 Texture 로 설정되었기 때문이고, 후자는 light probe 이 유효한 Texture 로 설정되었기 때문입니다.)

기본 성능 조사

씬의 리소스 및 성능 측면에 대한 기본적인 인사이트를 얻으려면 개발 프로세스 초기에 인터랙티브 DebugView 항목을 표시하는 방법을 추가하는 것이 좋습니다. 여기서는 오른쪽 상단에 고정된 DebugView 을 토글하는 Button 을 추가하기로 선택합니다.

import QtQuick
import QtQuick.Controls
import QtQuick3D
import QtQuick3D.Helpers

Item {
    width: 1280
    height: 720

    View3D {
        id: view3D
        anchors.fill: parent

        environment: SceneEnvironment {
            backgroundMode: SceneEnvironment.SkyBox
            lightProbe: Texture {
                textureData: ProceduralSkyTextureData {
                }
            }
        }

        PerspectiveCamera {
            id: camera
            y: 100
        }

        DirectionalLight {
        }

        Sponza {
            scale: Qt.vector3d(100, 100, 100)
        }

        PointLight {
            y: 200
            color: "#d9c62b"
            brightness: 5
            castsShadow: true
            shadowFactor: 75
            Model {
                source: "#Cube"
                scale: Qt.vector3d(0.01, 0.01, 0.01)
                materials: PrincipledMaterial {
                    lighting: PrincipledMaterial.NoLighting
                }
            }
        }

        Suzanne {
            y: 100
            scale: Qt.vector3d(50, 50, 50)
            NumberAnimation on eulerRotation.y {
                from: 0
                to: 360
                duration: 3000
                loops: Animation.Infinite
            }
        }

        WasdController {
            controlledObject: camera
        }
    }

    Button {
        anchors.right: parent.right
        text: "Toggle DebugView"
        onClicked: debugView.visible = !debugView.visible
        DebugView {
            id: debugView
            source: view3D
            visible: false
            anchors.top: parent.bottom
            anchors.right: parent.right
        }
    }
}

이 패널에서는 실시간 타이밍을 표시하고 텍스처 맵과 메시의 실시간 목록을 검토할 수 있으며 최종 컬러 버퍼를 렌더링하기 전에 수행해야 하는 렌더 패스에 대한 통찰력을 얻을 수 있습니다.

PointLight 을 그림자를 드리우는 조명으로 만들기 때문에 여러 렌더링 패스가 필요합니다:

Textures 섹션에는 수잔과 스폰자 에셋의 텍스처 맵(후자가 더 많음)과 절차적으로 생성된 하늘 텍스처가 있습니다.

Models 페이지에는 놀랄 만한 내용이 없습니다:

Tools 페이지에는 wireframe mode 과 다양한 material overrides 을 토글할 수 있는 대화형 컨트롤이 있습니다.

여기서는 와이어프레임 모드를 활성화하고 렌더링이 머티리얼의 base color 컴포넌트만 사용하도록 강제 설정한 상태입니다:

이것으로 임포트된 에셋을 사용하여 Qt Quick 3D 씬을 빌드하는 기초에 대한 설명을 마칩니다.

© 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.