이 페이지에서

OSM 건물

OSM(OpenStreetMap) 건물 지도 데이터를 볼 수 있는 3D 건물 뷰어입니다.

지도에 렌더링된 3D OpenStreetMap 건물들

개요

이 애플리케이션은 OpenStreetMap(OSM) 서버의 데이터를 사용하거나, 서버를 이용할 수 없을 때 로컬에 저장된 제한된 데이터 세트를 사용하여 지도에 표시할 3D 건물 지오메트리를 생성하는 방법을 보여줍니다.

대기열 처리

이 애플리케이션은 큐를 사용하여 동시 요청을 처리함으로써 지도 및 건물 데이터의 로딩 속도를 향상시킵니다.

OSMRequest::OSMRequest(QObject *parent)
    : QObject{parent}
{
    connect( &m_queuesTimer, &QTimer::timeout, this, [this](){
        if ( m_buildingsQueue.isEmpty() && m_mapsQueue.isEmpty() ) {
            m_queuesTimer.stop();
        }
        else {

#ifdef QT_DEBUG
            const int numConcurrentRequests = 1;
#else
            const int numConcurrentRequests = 6;
#endif
            if ( !m_buildingsQueue.isEmpty() && m_buildingsNumberOfRequestsInFlight < numConcurrentRequests ) {
                getBuildingsDataRequest(m_buildingsQueue.dequeue());
                ++m_buildingsNumberOfRequestsInFlight;
            }

            if ( !m_mapsQueue.isEmpty() && m_mapsNumberOfRequestsInFlight < numConcurrentRequests ) {
                getMapsDataRequest(m_mapsQueue.dequeue());
                ++m_mapsNumberOfRequestsInFlight;
            }
        }
    });
    m_queuesTimer.setInterval(0);
데이터 가져오기 및 구문 분석

OSM 건물 및 지도 서버에서 데이터를 가져오기 위해 사용자 정의 요청 핸들러 클래스가 구현되었습니다.

void OSMRequest::getBuildingsData(const QQueue<OSMTileData> &buildingsQueue) {

    if ( buildingsQueue.isEmpty() )
        return;
    m_buildingsQueue = buildingsQueue;
    if ( !m_queuesTimer.isActive() )
        m_queuesTimer.start();
}

void OSMRequest::getBuildingsDataRequest(const OSMTileData &tile)
{
    const QString fileName = "data/"_L1 + tileKey(tile) + ".json"_L1;
    QFileInfo file(fileName);
    if ( file.size() > 0 ) {
        QFile file(fileName);
        if (file.open(QFile::ReadOnly)){
            QByteArray data = file.readAll();
            file.close();
            emit buildingsDataReady( importGeoJson(QJsonDocument::fromJson( data )), tile.TileX, tile.TileY, tile.ZoomLevel );
            --m_buildingsNumberOfRequestsInFlight;
            return;
        }
    }

    QUrl url = QUrl(QString(URL_OSMB_JSON).arg(QString::number(tile.ZoomLevel),
                                               QString::number(tile.TileX),
                                               QString::number(tile.TileY),
                                               m_token));
    QNetworkReply* reply = m_networkAccessManager.get( QNetworkRequest(url));
    connect( reply, &QNetworkReply::finished, this, [this, reply, tile](){
        reply->deleteLater();
        if ( reply->error()== QNetworkReply::NoError ) {
            QByteArray data = reply->readAll();
            emit buildingsDataReady( importGeoJson(QJsonDocument::fromJson( data )), tile.TileX, tile.TileY, tile.ZoomLevel );
        } else {
            const QByteArray message = reply->readAll();
            static QByteArray lastMessage;
            if (message != lastMessage) {
                lastMessage = message;
                qWarning().noquote() << "OSMRequest::getBuildingsData " << reply->error()
                                    << reply->url() << message;
            }
        }
        --m_buildingsNumberOfRequestsInFlight;
    } );
void OSMRequest::getMapsData(const QQueue<OSMTileData> &mapsQueue) {

    if ( mapsQueue.isEmpty() )
        return;
    m_mapsQueue = mapsQueue;
    if ( !m_queuesTimer.isActive() )
        m_queuesTimer.start();
}

void OSMRequest::getMapsDataRequest(const OSMTileData &tile)
{
    const QString fileName = "data/"_L1 + tileKey(tile) + ".png"_L1;
    QFileInfo file(fileName);
    if ( file.size() > 0) {
        QFile file(fileName);
        if (file.open(QFile::ReadOnly)){
            QByteArray data = file.readAll();
            file.close();
            emit mapsDataReady(  data, tile.TileX, tile.TileY, tile.ZoomLevel );
            --m_mapsNumberOfRequestsInFlight;
            return;
        }
    }

    QUrl url = QUrl(QString(URL_OSMB_MAP).arg(QString::number(tile.ZoomLevel),
                                              QString::number(tile.TileX),
                                              QString::number(tile.TileY)));
    QNetworkReply* reply = m_networkAccessManager.get( QNetworkRequest(url));
    connect( reply, &QNetworkReply::finished, this, [this, reply, tile](){
        reply->deleteLater();
        if ( reply->error()== QNetworkReply::NoError ) {
            QByteArray data = reply->readAll();
            emit mapsDataReady( data, tile.TileX, tile.TileY, tile.ZoomLevel );
        } else {
            const QByteArray message = reply->readAll();
            static QByteArray lastMessage;
            if (message != lastMessage) {
                lastMessage = message;
                qWarning().noquote() << "OSMRequest::getMapsDataRequest" << reply->error()
                                    << reply->url() << message;
            }
        }
        --m_mapsNumberOfRequestsInFlight;
    } );

애플리케이션은 온라인 데이터를 파싱하여 QGeoPolygon 와 같은 지리적 형식의 키와 값으로 구성된 QVariant 목록으로 변환합니다.

            emit buildingsDataReady( importGeoJson(QJsonDocument::fromJson( data )), tile.TileX, tile.TileY, tile.ZoomLevel );
            --m_buildingsNumberOfRequestsInFlight;

구문 분석된 건물 데이터는 사용자 정의 지오메트리 항목으로 전송되어 지리적 좌표를 3D 좌표로 변환합니다.

    constexpr auto convertGeoCoordToVertexPosition = [](const float lat, const float lon) -> QVector3D {

        const double scale = 1.212;
        const double geoToPositionScale = 1000000 * scale;
        const double XOffsetFromCenter = 537277 * scale;
        const double YOffsetFromCenter = 327957 * scale;
        double x = (lon/360.0 + 0.5) * geoToPositionScale;
        double y = (1.0-log(qTan(qDegreesToRadians(lat)) + 1.0 / qCos(qDegreesToRadians(lat))) / M_PI) * 0.5 * geoToPositionScale;
        return QVector3D( x - XOffsetFromCenter, YOffsetFromCenter - y, 0.0 );
    };

인덱스 버퍼 및 버텍스 버퍼에 필요한 데이터(위치, 법선, 접선, UV 좌표 등)가 생성됩니다.

    for ( const QVariant &baseData : geoVariantsList ) {
        for ( const QVariant &dataValue : baseData.toMap()["data"_L1].toList() ) {
            const auto featureMap = dataValue.toMap();
            const auto properties = featureMap["properties"_L1].toMap();
            const auto buildingCoords = featureMap["data"_L1].value<QGeoPolygon>().perimeter();
            float height = 0.15 * properties["height"_L1].toLongLong();
            float levels = static_cast<float>(properties["levels"_L1].toLongLong());
            QColor color = QColor::fromString( properties["color"_L1].toString());
            if ( !color.isValid() || color == QColor(Qt::GlobalColor::black))
                color = QColor(Qt::GlobalColor::white);
            QColor roofColor = QColor::fromString( properties["roofColor"_L1].toString());
            if ( !roofColor.isValid() || roofColor == QColor(Qt::GlobalColor::black) )
                roofColor = color;

            QVector3D subsetMinBound = QVector3D(maxFloat, maxFloat, maxFloat);
            QVector3D subsetMaxBound = QVector3D(minFloat, minFloat, minFloat);

            qsizetype numSubsetVertices = buildingCoords.size() * 2;
            qsizetype lastVertexDataCount = vertexData.size();
            qsizetype lastIndexDataCount = indexData.size();
            vertexData.resize( lastVertexDataCount + numSubsetVertices * strideVertex );
            indexData.resize( lastIndexDataCount + ( numSubsetVertices - 2 ) * stridePrimitive );

            float *vbPtr = &reinterpret_cast<float *>(vertexData.data())[globalVertexCounter * strideVertexLen];
            uint32_t *ibPtr = &reinterpret_cast<uint32_t *>(indexData.data())[globalPrimitiveCounter * 3];

            qsizetype subsetVertexCounter = 0;

            QVector3D lastBaseVertexPos;
            QVector3D lastExtrudedVertexPos;
            QVector3D currentBaseVertexPos;
            QVector3D currentExtrudedVertexPos;
            QVector3D subsetPolygonCenter;

            using PolygonVertex = std::array<double, 2>;
            using PolygonVertices = std::vector<PolygonVertex>;

            PolygonVertices roofPolygonVertices;

            for ( const QGeoCoordinate &buildingPoint : buildingCoords ) {
   ...
                    std::vector<PolygonVertices> roofPolygonsVertices;
                    roofPolygonsVertices.push_back( roofPolygonVertices );
                    std::vector<uint32_t> roofIndices = mapbox::earcut<uint32_t>(roofPolygonsVertices);

                    lastVertexDataCount = vertexData.size();
                    lastIndexDataCount = indexData.size();
                    vertexData.resize( lastVertexDataCount + roofPolygonVertices.size() * strideVertex );
                    indexData.resize( lastIndexDataCount + roofIndices.size() * sizeof(uint32_t) );

                    vbPtr = &reinterpret_cast<float *>(vertexData.data())[globalVertexCounter * strideVertexLen];
                    ibPtr = &reinterpret_cast<uint32_t *>(indexData.data())[globalPrimitiveCounter * 3];

                    for ( const uint32_t &roofIndex : roofIndices ) {
                        *ibPtr++ = roofIndex + globalVertexCounter;
                    }
                    qsizetype roofPrimitiveCount = roofIndices.size() / 3;
                    globalPrimitiveCounter += roofPrimitiveCount;

                    for ( const PolygonVertex &polygonVertex : roofPolygonVertices ) {
                        QVector3D position{float(polygonVertex.at(0)),
                                           float(polygonVertex.at(1)), height};
                        QVector3D normal{0.0F, 0.0F, 1.0F};
                        QVector3D tangent{1.0F, 0.0F, 0.0F};
                        QVector3D binormal{0.0F, 1.0F, 0.0F};
                        writeVertex(vbPtr, position, normal, tangent, binormal, roofColor,
                                    1.0F, 1.0F, 1.0F, 0.0F, 1.0F);
                        ++subsetVertexCounter;
                        ++globalVertexCounter;
                    }

                }

            }
        }
    }

    clear();

구체를 그리는 코드는 OpenGL의 ‘Sphere’ 예제 코드를 참고하여 작성되었습니다.

다운로드한 PNG 데이터는 사용자 정의 ‘ QQuick3DTextureData ’ 항목으로 전송되어 PNG 형식을 맵 타일용 텍스처로 변환합니다.

void CustomTextureData::setImageData(const QByteArray &data)
{
    QImage image = QImage::fromData(data).convertToFormat(QImage::Format_RGBA8888);
    setTextureData( QByteArray(reinterpret_cast<const char*>(image.constBits()), image.sizeInBytes()) );
    setSize( image.size() );
    setHasTransparency(false);
    setFormat(Format::RGBA8);
}

이 애플리케이션은 카메라 위치, 방향, 확대/축소 수준 및 기울기 정보를 사용하여 화면에 표시된 영역 내에서 가장 가까운 타일을 찾습니다.

void OSMManager::setCameraProperties(const QVector3D &position, const QVector3D &right,
                                    float cameraZoom, float minimumZoom, float maximumZoom,
                                    float cameraTilt, float minimumTilt, float maximumTilt)
{

    float tiltFactor = (cameraTilt - minimumTilt) / qMax(maximumTilt - minimumTilt, 1.0);
    float zoomFactor = (cameraZoom - minimumZoom) / qMax(maximumZoom - minimumZoom, 1.0);

    // Forward vector align to the XY plane
    QVector3D forwardVector = QVector3D::crossProduct(right,
                                                      QVector3D(0.0, 0.0, -1.0)).normalized();
    QVector3D projectionOfForwardOnXY = position
            + forwardVector * tiltFactor * zoomFactor * 50.0;

    QQueue<OSMTileData> queue;
    for ( int forwardIndex = -20; forwardIndex <= 20; ++forwardIndex ){
        for ( int sidewardIndex = -20; sidewardIndex <= 20; ++sidewardIndex ){
            QVector3D transferredPosition = projectionOfForwardOnXY
                      + QVector3D(float(m_tileSizeX * sidewardIndex), float(m_tileSizeY * forwardIndex), 0.0);
            addBuildingRequestToQueue(queue, m_startBuildingTileX + int(transferredPosition.x() / m_tileSizeX),
                                m_startBuildingTileY - int(transferredPosition.y() / m_tileSizeY));
        }
    }

    const QPoint projectedTile{m_startBuildingTileX + int(projectionOfForwardOnXY.x() / m_tileSizeX),
                               m_startBuildingTileY - int(projectionOfForwardOnXY.y() / m_tileSizeY)};

    auto closer = [projectedTile](const OSMTileData &v1, const OSMTileData &v2) -> bool {
        return v1.distanceTo(projectedTile) < v2.distanceTo(projectedTile);
    };
    std::sort(queue.begin(), queue.end(), closer);

    m_request->getBuildingsData( queue );
    m_request->getMapsData( queue );

타일 요청 큐를 생성합니다.

void OSMManager::addBuildingRequestToQueue(QQueue<OSMTileData> &queue, int tileX, int tileY, int zoomLevel)
{
    OSMTileData data{tileX, tileY, zoomLevel};
제어

애플리케이션을 실행할 때, 탐색을 위해 다음 컨트롤을 사용하십시오.

창Android
이동마우스 왼쪽 버튼 + 드래그끌기
확대/축소마우스 휠핀치
회전마우스 오른쪽 버튼 + 드래그해당 사항 없음
        OSMCameraController {
            id: cameraController
            origin: originNode
            camera: cameraNode
        }
렌더링

맵 타일의 각 청크는 QML 모델(3D 지오메트리)과, 직사각형을 기반으로 타일맵 텍스처를 렌더링하는 사용자 정의 머티리얼로 구성됩니다.

        ...
        id: chunkModelMap
        Node {
            id: node
            property variant mapData: null
            property int tileX: 0
            property int tileY: 0
            property int zoomLevel: 0
            Model {
                id: basePlane
                position: Qt.vector3d( osmManager.tileSizeX * node.tileX, osmManager.tileSizeY * -node.tileY, 0.0 )
                scale: Qt.vector3d( osmManager.tileSizeX / 100., osmManager.tileSizeY / 100., 0.5)
                source: "#Rectangle"
                materials: [
                    CustomMaterial {
                        property TextureInput tileTexture: TextureInput {
                            enabled: true
                            texture: Texture {
                                textureData: CustomTextureData {
                                    Component.onCompleted: setImageData( node.mapData )
                                } }
                        }
                        shadingMode: CustomMaterial.Shaded
                        cullMode: Material.BackFaceCulling
                        fragmentShader: "customshadertiles.frag"
                    }
                ]
            }

이 애플리케이션은 사용자 정의 지오메트리를 사용하여 타일 건물을 렌더링합니다.

        ...
        id: chunkModelBuilding
        Node {
            id: node
            property variant geoVariantsList: null
            property int tileX: 0
            property int tileY: 0
            property int zoomLevel: 0
            Model {
                id: model
                scale: Qt.vector3d(1, 1, 1)

                OSMGeometry {
                    id: osmGeometry
                    Component.onCompleted: updateData( node.geoVariantsList )
                    onGeometryReady:{
                        model.geometry = osmGeometry
                    }
                }
                materials: [

                    CustomMaterial {
                        shadingMode: CustomMaterial.Shaded
                        cullMode: Material.BackFaceCulling
                        vertexShader: "customshaderbuildings.vert"
                        fragmentShader: "customshaderbuildings.frag"
                    }
                ]
            }

지붕과 같은 건물 부위를 하나의 드로우 콜로 렌더링하기 위해 사용자 정의 셰이더가 사용됩니다.

// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

VARYING vec4 color;

float rectangle(vec2 samplePosition, vec2 halfSize) {
    vec2 componentWiseEdgeDistance = abs(samplePosition) - halfSize;
    float outsideDistance = length(max(componentWiseEdgeDistance, 0.0));
    float insideDistance = min(max(componentWiseEdgeDistance.x, componentWiseEdgeDistance.y), 0.0);
    return outsideDistance + insideDistance;
}

void MAIN() {
    vec2 tc = UV0;
    vec2 uv = fract(tc * UV1.x); //UV1.x number of levels
    uv = uv * 2.0 - 1.0;
    uv.x = 0.0;
    uv.y = smoothstep(0.0, 0.2, rectangle( vec2(uv.x, uv.y + 0.5), vec2(0.2)) );
    BASE_COLOR = vec4(color.xyz * mix( clamp( ( vec3( 0.4, 0.4, 0.4 ) + tc.y)
                                             * ( vec3( 0.6, 0.6, 0.6 ) + uv.y)
                                             , 0.0, 1.0), vec3(1.0), UV1.y ), 1.0); // UV1.y as is roofTop
    ROUGHNESS = 0.3;
    METALNESS = 0.0;
    FRESNEL_POWER = 1.0;
}

예제 실행하기

다음 경로에서 예제를 실행할 수 있습니다:

code.qt.io의 예제 프로젝트

QML 애플리케이션도 참조하십시오 .

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