이 페이지에서

Qt Quick 3D Physics - 사용자 정의 도형 예시

다양한 도형을 사용하는 방법을 보여줍니다.

초록색 지형 위에 놓인, 투명한 직사각형 구조물 안에 실체적인 기하학적 도형들이 들어 있는 3D 장면의 스크린샷으로, 그 옆 화분에는 작은 다색 물체가 놓여 있다.

이 예제는 여러 리지드 바디 메쉬를 불러오고 생성하는 방법과 애니메이션 적용 방법을 보여줍니다. 이 씬은 주사위 타워, 식탁보, 컵, 그리고 몇 개의 주사위로 구성되어 있습니다. 컵은 생성된 주사위를 모아 주사위 타워에 넣는 애니메이션이 적용되어 있습니다. 주사위는 주사위 타워에서 굴러 내려와 식탁보 위로 흩어집니다.

환경

평소와 마찬가지로 ` PhysicsWorld `와 ` View3D`가 있습니다. ` View3D `에는 라이트프로브를 설정하는 환경이 포함되어 있습니다:

environment: SceneEnvironment {
    clearColor: "white"
    backgroundMode: SceneEnvironment.SkyBox
    antialiasingMode: SceneEnvironment.MSAA
    antialiasingQuality: SceneEnvironment.High
    lightProbe: proceduralSky
}

텍스처

스카이박스, 식탁보, 주사위 숫자에 사용될 네 가지 텍스처를 정의합니다:

Texture {
    id: proceduralSky
    textureData: ProceduralSkyTextureData {
        sunLongitude: -115
    }
}

Texture {
    id: weaveNormal
    source: "maps/weave.png"
    scaleU: 200
    scaleV: 200
    generateMipmaps: true
    mipFilter: Texture.Linear
}
Texture {
    id: numberNormal
    source: "maps/numbers-normal.png"
}

Texture {
    id: numberFill
    source: "maps/numbers.png"
    generateMipmaps: true
    mipFilter: Texture.Linear
}

씬

카메라와 방향성 조명이 포함된 씬을 담는 노드가 있습니다:

id: scene
scale: Qt.vector3d(2, 2, 2)
PerspectiveCamera {
    id: camera
    position: Qt.vector3d(-45, 25, 60)
    eulerRotation: Qt.vector3d(-6, -33, 0)
    clipFar: 1000
    clipNear: 0.1
}

DirectionalLight {
    eulerRotation: Qt.vector3d(-45, 25, 0)
    castsShadow: true
    brightness: 1
    shadowMapQuality: Light.ShadowMapQualityHigh
    pcfFactor: 0.1
    shadowBias: 1
}

식탁보

직조 텍스처가 적용된 모델과 충돌 처리를 위한 HeightFieldShape 로 구성된 ‘ StaticRigidBody ’인 식탁보를 추가합니다.

StaticRigidBody {
    position: Qt.vector3d(-15, -8, 0)
    id: tablecloth

    Model {
        geometry: HeightFieldGeometry {
            id: tableclothGeometry
            extents: Qt.vector3d(150, 20, 150)
            source: "maps/cloth-heightmap.png"
            smoothShading: false
        }
        materials: PrincipledMaterial {
            baseColor: "#447722"
            roughness: 0.8
            normalMap: weaveNormal
            normalStrength: 0.7
        }
    }

    collisionShapes: HeightFieldShape {
        id: hfShape
        extents: tableclothGeometry.extents
        source: "maps/cloth-heightmap.png"
    }
}

컵

컵은 모델(Model)과 충돌 형상( TriangleMeshShape )을 가진 DynamicRigidBody 로 정의합니다. 컵에는 애니메이션의 일부인 eulerRotation 및 position 속성에 대한 비헤이비어(Behavior)가 적용되어 있습니다.

DynamicRigidBody {
    id: diceCup
    isKinematic: true
    mass: 0
    property vector3d bottomPos: Qt.vector3d(11, 6, 0)
    property vector3d topPos: Qt.vector3d(11, 45, 0)
    property vector3d unloadPos: Qt.vector3d(0, 45, 0)
    position: bottomPos
    kinematicPivot: Qt.vector3d(0, 6, 0)
    kinematicPosition: bottomPos
    collisionShapes: TriangleMeshShape {
        id: cupShape
        source: "meshes/simpleCup.mesh"
    }
    Model {
        source: "meshes/cup.mesh"
        materials: PrincipledMaterial {
            baseColor: "#cc9988"
            roughness: 0.3
            metalness: 1
        }
    }
}

타워

타워는 단순히 모델(Model)과 충돌용 사각형( TriangleMeshShape )을 가진 StaticRigidBody 입니다.

StaticRigidBody {
    id: diceTower
    x: -4
    Model {
        id: testModel
        source: "meshes/tower.mesh"
        materials: [
            PrincipledMaterial {
                baseColor: "#ccccce"
                roughness: 0.3
            },
            PrincipledMaterial {
                id: glassMaterial
                baseColor: "#aaaacc"
                transmissionFactor: 0.95
                thicknessFactor: 1
                roughness: 0.05
            }
        ]
    }
    collisionShapes: TriangleMeshShape {
        id: triShape
        source: "meshes/tower.mesh"
    }
}

주사위

주사위를 생성하기 위해 사용자 정의 Die QML 객체와 Repeater3D 가 포함된 컴포넌트를 사용합니다.

Component {
    id: diceComponent
    Die {}
}

Repeater3D {
    id: dicePool
    model: 25
    delegate: diceComponent
    delegateModelAccess: DelegateModel.ReadWrite
    function restore() {
        for (let i = 0; i < count; i++) {
            (objectAt(i) as Die).restore()
        }
    }
}

Die 객체에는 ConvexMeshShape 와 Model이 포함된 DynamicRigidBody 가 들어 있습니다. 각 주사위의 위치, 색상, 스케일 및 메시 소스는 무작위로 생성됩니다.

DynamicRigidBody {
    id: thisBody
    required property int index

    Texture {
        id: numberNormal
        source: "maps/numbers-normal.png"
    }

    Texture {
        id: numberFill
        source: "maps/numbers.png"
        generateMipmaps: true
        mipFilter: Texture.Linear
    }

    function randomInRange(min, max) {
        return Math.random() * (max - min) + min
    }

    function restore() {
        reset(initialPosition, eulerRotation)
    }

    scale: Qt.vector3d(scaleFactor, scaleFactor, scaleFactor)
    eulerRotation: Qt.vector3d(randomInRange(0, 360),
                               randomInRange(0, 360),
                               randomInRange(0, 360))

    property vector3d initialPosition: Qt.vector3d(11 + 1.5 * Math.cos(index/(Math.PI/4)),
                                                   6 + index * 1.5,
                                                   0)
    position: initialPosition

    property real scaleFactor: randomInRange(0.8, 1.4)
    property color baseCol: Qt.hsla(randomInRange(0, 1),
                                    randomInRange(0.6, 1.0),
                                    randomInRange(0.4, 0.7),
                                    1.0)

    collisionShapes: ConvexMeshShape {
        id: diceShape
        source: Math.random() < 0.25 ? "meshes/icosahedron.mesh"
              : Math.random() < 0.5 ? "meshes/dodecahedron.mesh"
              : Math.random() < 0.75 ? "meshes/octahedron.mesh"
                                     : "meshes/tetrahedron.mesh"
    }

    Model {
        id: thisModel
        source: diceShape.source
        receivesShadows: false
        materials: PrincipledMaterial {
            metalness: 1.0
            roughness: thisBody.randomInRange(0.2, 0.6)
            baseColor: thisBody.baseCol
            emissiveMap: numberFill
            emissiveFactor: Qt.vector3d(1, 1, 1)
            normalMap: numberNormal
            normalStrength: 0.75
        }
    }
}

애니메이션

주사위가 컵에서 주사위 타워로 이동하도록 하기 위해, 컵에 애니메이션을 적용하여 위쪽으로 이동시킨 다음 뒤집습니다. 애니메이션이 물리 시뮬레이션과 동기화되도록 하기 위해, PhysicsWorld 의 onFrameDone 신호에 연결된 AnimationController 를 사용합니다. 시뮬레이션된 각 프레임이 끝날 때마다 경과된 타임스텝만큼 애니메이션을 진행합니다.

Connections {
    target: physicsWorld
    property real totalAnimationTime: 7500
    function onFrameDone(timeStep) {
        let progressStep = timeStep / totalAnimationTime
        animationController.progress += progressStep
        if (animationController.progress >= 1) {
            animationController.completeToEnd()
            animationController.reload()
            animationController.progress = 0
        }
    }
}

AnimationController {
    id: animationController
    animation: SequentialAnimation {
        PauseAnimation { duration: 2500 }
        PropertyAnimation {
            target: diceCup
            property: "kinematicPosition"
            to: diceCup.topPos
            duration: 2500
        }
        ParallelAnimation {
            PropertyAnimation {
                target: diceCup
                property: "kinematicEulerRotation.z"
                to: 130
                duration: 1500
            }
            PropertyAnimation {
                target: diceCup
                property: "kinematicPosition"
                to: diceCup.unloadPos
                duration: 1500
            }
        }
        PauseAnimation { duration: 1000 }
        ParallelAnimation {
            PropertyAnimation {
                target: diceCup
                property: "kinematicEulerRotation.z"
                to: 0
                duration: 1500
            }
            PropertyAnimation {
                target: diceCup
                property: "kinematicPosition"
                to: diceCup.topPos
                duration: 1500
            }
        }
        PropertyAnimation { target: diceCup; property: "kinematicPosition"; to: diceCup.bottomPos; duration: 1500 }
        PauseAnimation { duration: 2000 }
        ScriptAction { script: dicePool.restore() }
    }
}

컨트롤러

마지막으로 키보드를 사용하여 카메라를 제어할 수 있도록 WasdController 를 추가합니다:

WasdController {
    keysEnabled: true
    controlledObject: camera
    speed: 0.2
}

파일:

이미지:

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