Qt Quick 3D glTF 자산을 활용한 소개
'Qt Quick 3D - Introduction' 예제는 Qt Quick 3D 를 사용하여 QML 기반 애플리케이션을 만드는 방법을 간략하게 소개하지만, 이 예제에서는 구와 원통과 같은 내장 프리미티브만 사용합니다. 이 페이지에서는 Khronos 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
}
}
}
자산 가져오기
Sample Models 저장소에서 Sponza와 Suzanne이라는 두 개의 glTF 2.0 모델을 사용할 것입니다.
이 모델들은 일반적으로 .gltf 파일 외에도 여러 텍스처 맵과 별도의 바이너리 파일에 저장된 메쉬(지오메트리) 데이터를 포함하고 있습니다:

이 모든 것을 Qt Quick 3D 씬으로 어떻게 가져올 수 있을까요?
여러 가지 방법이 있습니다:
- 씬에서 인스턴스화할 수 있는 QML 컴포넌트를 생성하는 방법. 이 변환을 수행하는 명령줄 도구는 Balsam 도구입니다. 이 도구는 사실상 서브씬에 해당하는 .qml 파일을 생성할 뿐만 아니라, 메쉬(지오메트리) 데이터를 최적화되어 빠르게 로드되는 형식으로 재압축하고, 텍스처 맵 이미지 파일도 함께 복사합니다.
- Balsam의 GUI 프론트엔드인
balsamui을 사용하여 동일한 작업을 수행할 수 있습니다. - 를 사용하는 경우 Qt Design Studio를 사용하는 경우, 에셋 임포트 프로세스는 시각적 디자인 도구에 통합되어 있습니다. 예를 들어, .gltf 파일을 해당 패널로 드래그 앤 드롭하면 임포트가 시작됩니다.
- 특히 glTF 2.0 자산의 경우, RuntimeLoader 유형이라는 런타임 옵션도 있습니다. 이를 통해 Balsam과 같은 도구를 통한 사전 처리 없이도 런타임에 .gltf 파일(및 관련 바이너리 및 텍스처 데이터 파일)을 불러올 수 있습니다. 이는 사용자가 제공한 에셋을 열어 로드하려는 애플리케이션에서 매우 유용합니다. 반면, 성능 측면에서는 이 방식이 상당히 비효율적입니다. 따라서 이 소개 글에서는 이 방식에 대해서는 중점적으로 다루지 않겠습니다. 이 방식의 예시는 ‘Qt Quick 3D - RuntimeLoader Example’을 참고하시기 바랍니다.
balsam 와 balsamui 애플리케이션은 모두 Qt와 함께 제공되며, Qt Quick 3D 가 설치되거나 빌드된 상태라면 다른 유사한 실행 파일 도구들과 함께 해당 디렉터리에 존재할 것입니다. 대부분의 경우, 추가 인수를 지정할 필요 없이 명령줄에서 .gltf 파일을 대상으로 balsam을 실행하는 것만으로도 충분합니다. 그러나 balsamui 또는 Qt Design Studio 를 사용할 경우 제공되는 다양한 명령줄 옵션이나 대화형 옵션을 알아두는 것이 좋습니다. 예를 들어, 정적 전역 조명을 제공하기 위해 베이킹된 라이트맵을 다룰 때는, 실행 시점에 이 잠재적으로 시간이 많이 소요되는 프로세스를 수행하는 대신, 자산 가져오기 시점에 추가 라이트맵 UV 채널이 생성되도록 --generateLightmapUV 를 전달하고 싶을 가능성이 높습니다. 마찬가지로, 씬에서 자동 LOD를 활성화하기 위해 생성된 메시의 단순화된 버전을 사용하고자 할 때는 --generateMeshLevelsOfDetail 가 필수적입니다. 그 밖의 옵션으로는 누락된 데이터 생성(예: --generateNormals) 및 다양한 최적화 수행 등이 있습니다.
balsamui 에서는 명령줄 옵션이 대화형 요소로 매핑됩니다:

balsam을 통한 가져오기
시작해 봅시다! 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 로 활성화됨) 느낌이 자연스럽지 않습니다.
이를 해결하기 위해 ‘ 100 ’을 사용하여 Sponza 모델(서브씬)의 X, Y, Z 축 스케일을 조정합니다. 또한 카메라의 초기 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
}
}
}이 코드를 실행하면 다음과 같은 결과가 나옵니다:

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


참고: 위에서 “model”의 대안으로 subscene 을 여러 번언급했습니다 . 왜 그럴까요? Sponza 애셋의 경우 glTF 형식으로 103개의 서브메쉬를 가진 단일 모델이며, materials list 에 103개의 요소가 포함된 단일 Model 객체에 매핑되므로 명확하지 않을 수 있지만, 애셋은 각각 여러 서브메쉬와 관련 머티리얼을 가진 임의의 수의 models 를 포함할 수 있습니다. 이러한 모델들은 부모-자식 관계를 형성할 수 있으며, 추가적인 nodes 와 결합되어 평행 이동, 회전, 크기 조정과 같은 변환을 수행할 수 있습니다. 따라서 렌더링된 결과가 시각적으로 단일 모델로 인식되더라도, 가져온 에셋을 완전한 서브씬, 즉 nodes 의 임의의 트리 구조로 보는 것이 더 적절합니다. 생성된 Sponza.qml 또는 이러한 자산에서 생성된 다른 QML 파일을 일반 텍스트 편집기에서 열어 구조를 확인해 보세요(물론 구조는 소스 자산, 이 경우 glTF 파일이 어떻게 설계되었는지에 따라 달라집니다).
balsamui를 통한 임포트
두 번째 모델의 경우, 대신 balsam 의 그래픽 사용자 인터페이스를 사용해 보겠습니다.
balsamui 을 실행하면 도구가 열립니다:

Suzanne 모델을 가져와 봅시다. 이 모델은 텍스처 맵 두 개를 가진 비교적 단순한 모델입니다.

추가적인 구성 옵션이 필요하지 않으므로, 그냥 ‘변환(Convert)’을 클릭하면 됩니다. 결과는 balsam 을 실행했을 때와 동일합니다. 즉, 지정된 출력 디렉터리에 Suzanne.qml 파일과 몇 가지 추가 파일이 생성됩니다.

이 시점부터 생성된 자산을 다루는 방법은 이전 섹션과 동일합니다.
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
}
}
}다시 한 번, 인스턴스화된 Suzanne 노드에 스케일이 적용되고, 모델이 Sponza 건물의 바닥에 닿지 않도록 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 는 Suzanne 모델보다 약간 위쪽에 배치되어 있습니다. 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 파노라마 이미지를 반드시 구할 수 있는 것은 아니므로, 절차적으로 생성된 고동적 범위(HDRI) 하늘 이미지를 사용해 봅시다. 이는 ProceduralSkyTextureData 와 Texture 가 파일 기반이 아닌 동적으로 생성된 이미지 데이터를 지원하기 때문에 쉽게 구현할 수 있습니다. source 를 지정하는 대신, textureData 속성을 사용합니다.
environment: SceneEnvironment {
backgroundMode: SceneEnvironment.SkyBox
lightProbe: Texture {
textureData: ProceduralSkyTextureData {
}
}
}참고: 예제 코드에서는 객체를 인라인으로 정의하는 방식을 선호합니다. 이는 필수 사항은 아니며, SceneEnvironment 또는 ProceduralSkyTextureData 객체를 객체 트리의 다른 위치에 정의한 후 id 를 통해 참조할 수도 있습니다.
결과적으로, 스카이박스와 개선된 조명이 모두 구현되었습니다. (전자는 backgroundMode 가 SkyBox로 설정되고 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 섹션에서는 Suzanne 및 Sponza 에셋의 텍스처 맵(후자의 경우 텍스처 맵이 매우 많습니다)과 절차적으로 생성된 하늘 텍스처를 확인할 수 있습니다.

Models 페이지에는 특별한 점이 없습니다:

' Tools ' 페이지에는 ' wireframe mode ' 및 다양한 ' material overrides'을 토글할 수 있는 몇 가지 상호작용형 컨트롤이 있습니다.
여기서는 와이어프레임 모드를 활성화하고, 렌더링 시 재료의 ‘ base color ’ 구성 요소만 사용하도록 강제 설정했습니다:

이것으로 가져온 에셋을 사용한 Qt Quick 3D 장면을 구축하는 방법에 대한 소개를 마치겠습니다.
© 2026 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.