Initial commit

This commit is contained in:
2026-04-29 07:19:21 +03:00
commit 9a8cdfa08a
5964 changed files with 1194660 additions and 0 deletions
@@ -0,0 +1,70 @@
// Copyright (C) 2025 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
import QtQuick
import QtQuick3D
// The rotation math is based on the paper
// ARCBALL:
// A User Interface for Specifying Three-Dimensional Orientation Using a Mouse
// by Ken Shoemake, 1992
Item {
id: root
visible: false
required property Node controlledObject
property vector3d lastPos: Qt.vector3d(0, 0, 0)
property bool moving: false
// From Shoemake 1992:
// pt.x <- (screen.x - center.x)/radius;
// pt.y <- (screen.y - center.y)/radius;
// r <- pt.x*pt.x + pt.y*pt.y;
// IF r > 1.0
// THEN s <- 1.0/Sqrt[r];
// pt.x <- s*pt.x;
// pt.y <- s*pt.y;
// pt.z <- 0.0;
// ELSE pt.z <- Sqrt[1.0 - r] ;
function pos2DToPos3D(posNDC) {
var pt = Qt.vector3d(posNDC.x, posNDC.y, 0)
let r = posNDC.x * posNDC.x + posNDC.y * posNDC.y
if (r > 1.0) {
let s = 1.0 / Math.sqrt(r)
pt.x = s * pt.x
pt.y = s * pt.y
pt.z = 0.0
} else {
pt.z = Math.sqrt(1.0 - r)
}
return pt
}
function mousePressed(posNDC) {
lastPos = pos2DToPos3D(posNDC)
moving = true
}
function mouseReleased(posNDC) {
moving = false
}
function mouseMoved(posNDC) {
if (!moving)
return
let currentPos = pos2DToPos3D(posNDC)
// From Shoemake 1992:
// [q.x, q.y, q.z] <- V3_Cross[pO, p1];
// q.w <- V3_Dot[pO, p1];
// qnow <- QuatMul[q, qstart];
let p0 = lastPos
let p1 = currentPos
let p0p1 = p0.crossProduct(p1)
let q = Qt.quaternion(p0.dotProduct(p1), p0p1.x, p0p1.y, p0p1.z)
let qnow = q.times(controlledObject.rotation)
controlledObject.rotation = qnow
lastPos = currentPos
}
}
@@ -0,0 +1,135 @@
// Copyright (C) 2025 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
import QtQuick
import LightmapFile 1.0
Rectangle {
id: scrollView
clip: true
color: "black"
property real lastMouseX: 0
property real lastMouseY: 0
function clamp() {
// If the image is smaller than the scroll view, center it
if (image.width <= scrollView.width) {
imageCenterX = 0
} else {
const maxOffsetX = (image.width - scrollView.width) / 2
imageCenterX = Math.max(-maxOffsetX,
Math.min(imageCenterX,
maxOffsetX))
}
if (image.height <= scrollView.height) {
imageCenterY = 0
} else {
const maxOffsetY = (image.height - scrollView.height) / 2
imageCenterY = Math.max(-maxOffsetY,
Math.min(imageCenterY,
maxOffsetY))
}
}
onWidthChanged: clamp()
onHeightChanged: clamp()
Connections {
target: window
function onSelectedEntryChanged() {
if (imageLoader.item === scrollView) {
imageZoom = 1
imageCenterX = 0
imageCenterY = 0
}
}
}
MouseArea {
id: mouseArea
property bool dragging: false
anchors.fill: parent
onPressed: mouse => {
scrollView.lastMouseX = mouse.x
scrollView.lastMouseY = mouse.y
dragging = true
}
onReleased: mouse => {
dragging = false
}
onPositionChanged: mouse => {
var dx = mouse.x - scrollView.lastMouseX
var dy = mouse.y - scrollView.lastMouseY
scrollView.lastMouseX = mouse.x
scrollView.lastMouseY = mouse.y
imageCenterX += dx
imageCenterY += dy
clamp()
}
cursorShape: mouseArea.dragging ? Qt.ClosedHandCursor : Qt.ArrowCursor
onWheel: event => {
const oldZoom = imageZoom
const zoomDelta = event.angleDelta.y / 256
const newZoom = Math.max(
1, Math.min(32, oldZoom + zoomDelta))
if (newZoom === oldZoom)
return
// Adjust center offset so the same point remains at the center
const scaleFactor = newZoom / oldZoom
imageCenterX *= scaleFactor
imageCenterY *= scaleFactor
imageZoom = newZoom
clamp()
event.accepted = true
}
}
Image {
id: baseGrid
anchors.fill: scrollView
source: "grid.png"
fillMode: Image.Tile
opacity: 0.75
}
Rectangle {
width: image.width + (border.width * 2)
height: image.height + (border.width * 2)
x: image.x - border.width
y: image.y - border.width
color: "white" // This is the border color
border.width: 0
border.color: "white"
opacity: 0.25
visible: window.isImage(window.selectedEntry)
}
Image {
id: image
x: Math.round(parent.width / 2 - width / 2) + imageCenterX
y: Math.round(parent.height / 2 - height / 2) + imageCenterY
source: window.isImage(
window.selectedEntry) ? `image://lightmaps/key=${selectedEntry.key}&tag=${selectedEntry.tag}&file=${LightmapFile.source}&alpha=${alphaSwitch.checked}` : ""
onWidthChanged: clamp()
onHeightChanged: clamp()
fillMode: Image.PreserveAspectFit
smooth: false
antialiasing: false
// Let the image scale visibly
width: sourceSize.width * imageZoom
height: sourceSize.height * imageZoom
}
}
@@ -0,0 +1,325 @@
// Copyright (C) 2025 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Window
import QtQuick.Dialogs
import QtQuick3D
import QtQuick3D.Helpers
import QtQuick3D.lightmapviewer
import LightmapFile 1.0
ApplicationWindow {
width: 1024
height: 768
visible: true
title: qsTr("Lightmap Viewer")
id: window
property var selectedEntry: listView.model.length ? listView.model[0] : null
property real imageZoom: 1
property real imageCenterX: 0
property real imageCenterY: 0
function isImage(entry) {
return entry && entry.kind === "image"
}
function isMesh(entry) {
return entry && entry.kind === "mesh"
}
Dialog {
id: sceneMetadataDialog
modal: true
standardButtons: Dialog.NoButton
x: Math.round((window.width - width) / 2)
y: Math.round((window.height - height) / 2)
visible: false
width: 220
height: 360
contentItem: SceneMetadataView {}
}
header: ToolBar {
RowLayout {
Button {
text: qsTr("Open Lightmap...")
onClicked: fileDialog.open()
}
Rectangle {
width: 1
color: "darkgray"
Layout.fillHeight: true
Layout.alignment: Qt.AlignVCenter
}
Button {
text: qsTr("Scene Metadata...")
onClicked: sceneMetadataDialog.open()
}
Label {
text: "Zoom: " + window.imageZoom.toFixed(1)
}
Rectangle {
width: 1
color: "darkgray"
Layout.fillHeight: true
Layout.alignment: Qt.AlignVCenter
}
Switch {
id: alphaSwitch
padding: 0
checked: true
text: "Alpha"
}
Rectangle {
width: 1
color: "darkgray"
Layout.fillHeight: true
Layout.alignment: Qt.AlignVCenter
}
Text {
text: "Path: " + LightmapFile.source
}
}
}
FileDialog {
id: fileDialog
onAccepted: {
LightmapFile.source = selectedFile
LightmapFile.loadData()
}
}
Shortcut {
sequences: [StandardKey.Open]
onActivated: {
fileDialog.open()
}
}
SplitView {
anchors.fill: parent
orientation: Qt.Horizontal
focus: true
Keys.onPressed: event => {
if (event.key === Qt.Key_Up) {
listView.currentIndex = Math.max(
0, listView.currentIndex - 1)
selectedEntry = listView.model[listView.currentIndex]
} else if (event.key === Qt.Key_Down) {
listView.currentIndex = Math.min(
listView.model.length - 1,
listView.currentIndex + 1)
selectedEntry = listView.model[listView.currentIndex]
}
}
SplitView {
id: leftSplit
SplitView.preferredWidth: 220
SplitView.minimumWidth: 120
orientation: Qt.Vertical
Item {
id: metaArea
SplitView.preferredHeight: 120
anchors.left: parent.left
anchors.right: parent.right
ColumnLayout {
anchors.fill: parent
spacing: 4
Pane {
id: metaPane
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
ScrollView {
anchors.fill: parent
ColumnLayout {
id: metadataColumn
Layout.fillWidth: true
spacing: 4
Repeater {
model: LightmapFile.metadataFor(selectedEntry)
delegate: RowLayout {
width: metadataColumn.width
spacing: 8
Label {
text: (modelData.key ?? "—") + ":"
font.bold: true
}
Label {
text: modelData.value
!== undefined ? String(
modelData.value) : "—"
Layout.fillWidth: true
}
}
}
}
}
}
}
}
ListView {
id: listView
clip: true
spacing: 2
highlightMoveVelocity: -1
highlightMoveDuration: 1
model: LightmapFile.dataList
property var sectionExpanded: ({})
section.property: "owner"
section.criteria: ViewSection.FullString
section.delegate: Rectangle {
width: listView.width
height: 26
color: Qt.rgba(0, 0, 0, 0.05)
radius: 4
Row {
anchors.fill: parent
anchors.leftMargin: 8
anchors.rightMargin: 8
spacing: 6
anchors.verticalCenter: parent.verticalCenter
Text {
text: (listView.sectionExpanded[section] === false) ? "▸" : "▾"
verticalAlignment: Text.AlignVCenter
}
Text {
text: section
font.bold: true
verticalAlignment: Text.AlignVCenter
}
}
MouseArea {
anchors.fill: parent
onClicked: {
listView.sectionExpanded[section]
= !(listView.sectionExpanded[section] !== false)
listView.sectionExpanded = Object.assign(
{}, listView.sectionExpanded)
}
}
}
delegate: Item {
width: listView.width
property bool isExpanded: listView.sectionExpanded[modelData.owner] !== false
height: isExpanded ? Math.max(
24, rowText.implicitHeight + 6) : 0
opacity: isExpanded ? 1 : 0
Behavior on height {
NumberAnimation {
duration: 120
easing.type: Easing.OutCubic
}
}
Behavior on opacity {
NumberAnimation {
duration: 120
easing.type: Easing.OutCubic
}
}
Row {
anchors.fill: parent
anchors.leftMargin: 16
anchors.rightMargin: 8
anchors.verticalCenter: parent.verticalCenter
Text {
id: rowText
text: modelData.display
elide: Text.ElideRight
}
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
enabled: isExpanded
onClicked: {
listView.currentIndex = index
selectedEntry = modelData
}
}
}
highlight: Rectangle {
color: Qt.rgba(76 / 255, 134 / 255, 191 / 255, 0.25)
radius: 6
anchors.margins: 2
}
ScrollBar.vertical: ScrollBar {}
}
}
Item {
id: rightSplit
SplitView.fillWidth: true
SplitView.fillHeight: true
// These are toggled based on what is currently selected
Loader {
id: imageLoader
anchors.fill: parent
sourceComponent: ImageViewer {}
active: true
visible: isImage(selectedEntry)
enabled: visible
}
Loader {
id: meshLoader
anchors.fill: parent
sourceComponent: MeshViewer {}
active: true
visible: isMesh(selectedEntry)
enabled: visible
}
}
}
DropArea {
id: dropArea
anchors.fill: parent
onEntered: drag => {
drag.accept(Qt.LinkAction)
}
// Just take first url if several
onDropped: drop => {
if (drop.hasUrls) {
LightmapFile.source = drop.urls[0]
LightmapFile.loadData()
}
}
}
}
@@ -0,0 +1,314 @@
// Copyright (C) 2025 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Window
import QtQuick.Dialogs
import QtQuick3D
import QtQuick3D.Helpers
import QtQuick3D.lightmapviewer
import LightmapFile 1.0
ColumnLayout {
id: root
anchors.fill: parent
property string selectedModelCandidate: ""
onSelectedModelCandidateChanged: {
if (!selectedModelCandidate || selectedModelCandidate === "") {
view.lmTextureCandidates = []
lview.mSelectedTextureCandidate = -1
return
}
view.lmTextureCandidates = LightmapFile.texturesAvailableFor(
selectedModelCandidate)
view.lmSelectedTextureCandidate
= view.lmTextureCandidates.length ? view.lmTextureCandidates[0].value : -1
}
Pane {
Layout.fillWidth: true
padding: 8
ColumnLayout {
width: parent.width
spacing: 6
ColumnLayout {
RowLayout {
Label {
text: "Key:"
}
ComboBox {
id: comboLmModelCandidate
Layout.preferredWidth: 220
model: view.lmModelCandidates
onActivated: root.selectedModelCandidate = currentText
currentIndex: {
const i = view.lmModelCandidates.indexOf(
root.selectedModelCandidate)
return (i >= 0 ? i : -1)
}
enabled: !!selectedEntry
&& selectedEntry.kind === "mesh"
}
Label {
text: "Texture:"
}
ComboBox {
id: comboLmTextureCandidate
Layout.preferredWidth: 220
model: view.lmTextureCandidates
textRole: "name"
valueRole: "value"
function indexForValue(val) {
for (var i = 0; i < view.lmTextureCandidates.length; ++i)
if (view.lmTextureCandidates[i].value === val)
return i
return -1
}
onActivated: view.lmSelectedTextureCandidate = Number(
currentValue)
currentIndex: indexForValue(
view.lmSelectedTextureCandidate)
enabled: !!selectedEntry
&& selectedEntry.kind === "mesh"
}
}
RowLayout {
CheckBox {
id: checkboxBfCull
text: "Backface Culling"
checked: true
}
CheckBox {
id: checkboxDebugUV
text: "Debug UV"
checked: false
}
}
}
}
}
View3D {
id: view
Layout.fillWidth: true
Layout.fillHeight: true
enabled: meshLoader.visible
visible: meshLoader.visible
property var lmModelCandidates: []
property var lmTextureCandidates: []
property int lmSelectedTextureCandidate: -1
property real boundsDiameter: 0
property vector3d boundsCenter
property vector3d boundsSize
function updateBounds(bounds) {
boundsSize = Qt.vector3d(bounds.maximum.x - bounds.minimum.x,
bounds.maximum.y - bounds.minimum.y,
bounds.maximum.z - bounds.minimum.z)
boundsDiameter = Math.max(boundsSize.x, boundsSize.y, boundsSize.z)
boundsCenter = Qt.vector3d(
(bounds.maximum.x + bounds.minimum.x) / 2,
(bounds.maximum.y + bounds.minimum.y) / 2,
(bounds.maximum.z + bounds.minimum.z) / 2)
model.position = Qt.vector3d(-boundsCenter.x, -boundsCenter.y,
-boundsCenter.z)
cameraNode.clipNear = boundsDiameter / 100
cameraNode.clipFar = boundsDiameter * 10
resetCamera()
}
function resetCamera() {
cameraNode.position = boundsCenter
cameraNode.position = Qt.vector3d(0, 0, 2 * boundsDiameter)
cameraNode.eulerRotation = Qt.vector3d(0, 0, 0)
}
function refreshLightmapSelection() {
if (!selectedEntry) {
lmModelCandidates = []
root.selectedModelCandidate = ""
lmTextureCandidates = []
lmSelectedTextureCandidate = -1
return
}
if (selectedEntry.kind === "image") {
lmModelCandidates = [selectedEntry.key]
root.selectedModelCandidate = selectedEntry.key
} else if (selectedEntry.kind === "mesh") {
lmModelCandidates = LightmapFile.keysReferencingMesh(
selectedEntry.key)
root.selectedModelCandidate = lmModelCandidates.length ? lmModelCandidates[0] : ""
} else {
lmModelCandidates = []
root.selectedModelCandidate = ""
lmTextureCandidates = []
lmSelectedTextureCandidate = -1
}
}
Component.onCompleted: refreshLightmapSelection()
Connections {
target: window
function onSelectedEntryChanged() {
view.refreshLightmapSelection()
}
}
environment: SceneEnvironment {
backgroundMode: SceneEnvironment.Color
clearColor: "black"
}
PerspectiveCamera {
id: cameraNode
z: 300
}
Node {
id: modelNode
Model {
id: model
geometry: LightmapMesh {
source: LightmapFile.source
key: selectedEntry.key
onBoundsChanged: view.updateBounds(bounds)
}
materials: CustomMaterial {
shadingMode: CustomMaterial.Unshaded
cullMode: checkboxBfCull.checked ? Material.BackFaceCulling : Material.NoCulling
property TextureInput baseMap: TextureInput {
texture: Texture {
id: lmTexture
minFilter: Texture.Linear
magFilter: Texture.Linear
mipFilter: Texture.None
tilingModeHorizontal: Texture.ClampToEdge
tilingModeVertical: Texture.ClampToEdge
textureData: LightmapFile.textureDataFor(
root.selectedModelCandidate,
view.lmSelectedTextureCandidate)
}
}
property bool debugUV: checkboxDebugUV.checked
vertexShader: "mesh.vert"
fragmentShader: "mesh.frag"
}
}
}
ArcballController {
id: arcballController
controlledObject: modelNode
function jumpToAxis(axis) {
cameraRotation.from = arcballController.controlledObject.rotation
cameraRotation.to = originGizmo.quaternionForAxis(
axis, arcballController.controlledObject.rotation)
cameraRotation.duration = 200
cameraRotation.start()
}
function jumpToRotation(qRotation) {
cameraRotation.from = arcballController.controlledObject.rotation
cameraRotation.to = qRotation
cameraRotation.duration = 100
cameraRotation.start()
}
QuaternionAnimation {
id: cameraRotation
target: arcballController.controlledObject
property: "rotation"
type: QuaternionAnimation.Slerp
running: false
loops: 1
}
}
OriginGizmo {
id: originGizmo
anchors.top: parent.top
anchors.right: parent.right
anchors.margins: 10
width: 120
height: 120
targetNode: modelNode
onAxisClicked: axis => {
arcballController.jumpToAxis(axis)
}
}
DragHandler {
id: dragHandler
target: null
acceptedModifiers: Qt.NoModifier
onCentroidChanged: {
arcballController.mouseMoved(toNDC(centroid.position.x,
centroid.position.y))
}
onActiveChanged: {
if (active) {
view.forceActiveFocus()
arcballController.mousePressed(toNDC(centroid.position.x,
centroid.position.y))
} else
arcballController.mouseReleased(toNDC(centroid.position.x,
centroid.position.y))
}
function toNDC(x, y) {
return Qt.vector2d((2.0 * x / width) - 1.0,
1.0 - (2.0 * y / height))
}
}
WheelHandler {
id: wheelHandler
orientation: Qt.Vertical
target: null
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
onWheel: event => {
let delta = -event.angleDelta.y * 0.01
cameraNode.z += cameraNode.z * 0.1 * delta
}
}
Keys.onPressed: event => {
if (event.key === Qt.Key_Space) {
let rotation = originGizmo.quaternionAlign(
arcballController.controlledObject.rotation)
arcballController.jumpToRotation(rotation)
} else if (event.key === Qt.Key_S) {
settingsPane.toggleHide()
} else if (event.key === Qt.Key_Left
|| event.key === Qt.Key_A) {
let rotation = originGizmo.quaternionRotateLeft(
arcballController.controlledObject.rotation)
arcballController.jumpToRotation(rotation)
} else if (event.key === Qt.Key_Right
|| event.key === Qt.Key_D) {
let rotation = originGizmo.quaternionRotateRight(
arcballController.controlledObject.rotation)
arcballController.jumpToRotation(rotation)
}
}
}
}
@@ -0,0 +1,401 @@
// Copyright (C) 2025 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
import QtQuick
import QtQuick3D
Item {
id: root
required property Node targetNode
enum Axis {
PositiveZ = 0,
NegativeZ = 1,
PositiveY = 2,
NegativeY = 3,
PositiveX = 4,
NegativeX = 5
}
// These are the 24 different rotations a rotation aligned on axes can have.
// They are ordered in groups of 4 where the +Z,-Z,+Y,-Y,+X,-X axis is pointing
// towards the screen (+Z). Inside this group the rotations are ordered to
// rotate counter-clockwise.
readonly property list<quaternion> rotations: [
// +Z
Qt.quaternion(1, 0, 0, 0),
Qt.quaternion(Math.SQRT1_2, 0, 0, -Math.SQRT1_2),
Qt.quaternion(0, 0, 0, -1),
Qt.quaternion(Math.SQRT1_2, 0, 0, Math.SQRT1_2),
// -Z
Qt.quaternion(0, 0, -1, 0),
Qt.quaternion(0, -Math.SQRT1_2, -Math.SQRT1_2, 0),
Qt.quaternion(0, -1, 0, 0),
Qt.quaternion(0, Math.SQRT1_2, -Math.SQRT1_2, 0),
// +Y
Qt.quaternion(0.5, 0.5, 0.5, 0.5),
Qt.quaternion(Math.SQRT1_2, Math.SQRT1_2, 0, 0),
Qt.quaternion(-0.5, -0.5, 0.5, 0.5),
Qt.quaternion(0, 0, -Math.SQRT1_2, -Math.SQRT1_2),
// -Y
Qt.quaternion(0.5, -0.5, 0.5, -0.5),
Qt.quaternion(0, 0, Math.SQRT1_2, -Math.SQRT1_2),
Qt.quaternion(-0.5, 0.5, 0.5, -0.5),
Qt.quaternion(-Math.SQRT1_2, Math.SQRT1_2, 0, 0),
// +X
Qt.quaternion(-0.5, -0.5, 0.5, -0.5),
Qt.quaternion(-Math.SQRT1_2, 0, Math.SQRT1_2, 0),
Qt.quaternion(-0.5, 0.5, 0.5, 0.5),
Qt.quaternion(0, Math.SQRT1_2, 0, Math.SQRT1_2),
// -X
Qt.quaternion(0, Math.SQRT1_2, 0, -Math.SQRT1_2),
Qt.quaternion(0.5, -0.5, 0.5, 0.5),
Qt.quaternion(Math.SQRT1_2, 0, Math.SQRT1_2, 0),
Qt.quaternion(0.5, 0.5, 0.5, -0.5),
]
readonly property list<quaternion> xRotationGoals : [
Qt.quaternion(0, 1, 0, 0),
Qt.quaternion(0, 0, -1, 0),
Qt.quaternion(0, -1, 0, 0),
Qt.quaternion(0, 0, 1, 0),
Qt.quaternion(0, -1, 0, 0),
Qt.quaternion(0, 0, 1, 0),
Qt.quaternion(0, 1, 0, 0),
Qt.quaternion(0, 0, -1, 0),
Qt.quaternion(0, 0, 1, 0),
Qt.quaternion(0, 1, 0, 0),
Qt.quaternion(0, 0, -1, 0),
Qt.quaternion(-0, -1, -0, -0),
Qt.quaternion(0, 0, -1, 0),
Qt.quaternion(-0, -1, -0, -0),
Qt.quaternion(0, 0, 1, 0),
Qt.quaternion(-0, 1, -0, -0),
Qt.quaternion(0, 0, 0, 1),
Qt.quaternion(0, 0, 0, 1),
Qt.quaternion(0, 0, 0, 1),
Qt.quaternion(0, 0, 0, 1),
Qt.quaternion(0, 0, 0, -1),
Qt.quaternion(0, 0, 0, -1),
Qt.quaternion(0, 0, 0, -1),
Qt.quaternion(0, 0, 0, -1),
]
readonly property list<quaternion> yRotationGoals : [
Qt.quaternion(0, 0, 1, 0),
Qt.quaternion(0, 1, 0, 0),
Qt.quaternion(0, 0, -1, 0),
Qt.quaternion(0, -1, 0, 0),
Qt.quaternion(0, 0, 1, 0),
Qt.quaternion(0, 1, 0, 0),
Qt.quaternion(0, 0, -1, 0),
Qt.quaternion(0, -1, 0, 0),
Qt.quaternion(0, 0, 0, 1),
Qt.quaternion(0, 0, 0, 1),
Qt.quaternion(0, 0, 0, 1),
Qt.quaternion(0, 0, 0, 1),
Qt.quaternion(0, 0, 0, -1),
Qt.quaternion(0, 0, 0, -1),
Qt.quaternion(0, 0, 0, -1),
Qt.quaternion(0, 0, 0, -1),
Qt.quaternion(0, -1, 0, 0),
Qt.quaternion(0, 0, 1, 0),
Qt.quaternion(0, 1, 0, 0),
Qt.quaternion(0, 0, -1, 0),
Qt.quaternion(0, 0, -1, 0),
Qt.quaternion(0, -1, 0, 0),
Qt.quaternion(0, 0, 1, 0),
Qt.quaternion(0, 1, 0, 0),
]
readonly property list<quaternion> zRotationGoals : [
Qt.quaternion(0, 0, 0, 1),
Qt.quaternion(0, 0, 0, 1),
Qt.quaternion(0, 0, 0, 1),
Qt.quaternion(0, 0, 0, 1),
Qt.quaternion(0, 0, 0, -1),
Qt.quaternion(0, 0, 0, -1),
Qt.quaternion(0, 0, 0, -1),
Qt.quaternion(0, 0, 0, -1),
Qt.quaternion(0, 1, 0, 0),
Qt.quaternion(0, 0, -1, 0),
Qt.quaternion(0, -1, 0, 0),
Qt.quaternion(0, 0, 1, 0),
Qt.quaternion(0, 1, 0, 0),
Qt.quaternion(0, 0, -1, 0),
Qt.quaternion(0, -1, 0, 0),
Qt.quaternion(0, 0, 1, 0),
Qt.quaternion(0, 0, -1, 0),
Qt.quaternion(0, -1, 0, 0),
Qt.quaternion(0, 0, 1, 0),
Qt.quaternion(0, 1, 0, 0),
Qt.quaternion(0, -1, 0, 0),
Qt.quaternion(0, 0, 1, 0),
Qt.quaternion(0, 1, 0, 0),
Qt.quaternion(0, 0, -1, 0),
]
// This function works by using a rotation to rotate x,y,z normal vectors
// and see what axis-aligned rotation gives the closest distance to the
// rotated normal vectors.
function findClosestRotation(rotation, startI, stopI) {
let rotationConjugated = rotation.conjugated();
let xRotated = rotation.times(Qt.quaternion(0, 1, 0, 0)).times(rotationConjugated);
let yRotated = rotation.times(Qt.quaternion(0, 0, 1, 0)).times(rotationConjugated);
let zRotated = rotation.times(Qt.quaternion(0, 0, 0, 1)).times(rotationConjugated);
var closestIndex = 0;
var closestDistance = 123456789; // big number
for (var i = startI; i < stopI ; i++) {
let distance = xRotated.minus(xRotationGoals[i]).length() +
yRotated.minus(yRotationGoals[i]).length() +
zRotated.minus(zRotationGoals[i]).length();
if (distance <= closestDistance) {
closestDistance = distance;
closestIndex = i;
}
}
return closestIndex;
}
function quaternionAlign(rotation) {
let closestIndex = findClosestRotation(rotation, 0, 24);
return rotations[closestIndex];
}
function quaternionForAxis(axis, rotation) {
let closestIndex = findClosestRotation(rotation, axis*4, (axis + 1)*4);
return rotations[closestIndex];
}
function quaternionRotateLeft(rotation) {
let closestIndex = findClosestRotation(rotation, 0, 24);
let offset = (4 + closestIndex - 1) % 4;
let group = Math.floor(closestIndex / 4);
return rotations[offset + group * 4];
}
function quaternionRotateRight(rotation) {
let closestIndex = findClosestRotation(rotation, 0, 24);
let offset = (closestIndex + 1) % 4;
let group = Math.floor(closestIndex / 4);
return rotations[offset + group * 4];
}
signal axisClicked(int axis)
signal ballMoved(vector2d velocity)
QtObject {
id: stylePalette
property color white: "#fdf6e3"
property color black: "#002b36"
property color red: "#dc322f"
property color green: "#859900"
property color blue: "#268bd2"
property color background: "#99002b36"
}
component LineRectangle : Rectangle {
property vector2d startPoint: Qt.vector2d(0, 0)
property vector2d endPoint: Qt.vector2d(0, 0)
property real lineWidth: 5
transformOrigin: Item.Left
height: lineWidth
readonly property vector2d offset: startPoint.plus(endPoint).times(0.5);
width: startPoint.minus(endPoint).length()
rotation: Math.atan2(endPoint.y - startPoint.y, endPoint.x - startPoint.x) * 180 / Math.PI
}
Rectangle {
id: ballBackground
anchors.centerIn: parent
width: parent.width > parent.height ? parent.height : parent.width
height: width
radius: width / 2
color: ballBackgroundHoverHandler.hovered ? stylePalette.background : "transparent"
readonly property real subBallWidth: width / 5
readonly property real subBallHalfWidth: subBallWidth * 0.5
readonly property real subBallOffset: radius - subBallWidth / 2
Item {
anchors.centerIn: parent
component SubBall : Rectangle {
id: subBallRoot
required property Node targetNode
required property real offset
property alias labelText: label.text
property alias labelColor: label.color
property alias labelVisible: label.visible
property alias hovered: subBallHoverHandler.hovered
property var initialPosition: Qt.vector3d(0, 0, 0)
readonly property vector3d position: quaternionVectorMultiply(targetNode.rotation, initialPosition)
signal tapped()
function quaternionVectorMultiply(q, v) {
var qv = Qt.vector3d(q.x, q.y, q.z)
var uv = qv.crossProduct(v)
var uuv = qv.crossProduct(uv)
uv = uv.times(2.0 * q.scalar)
uuv = uuv.times(2.0)
return v.plus(uv).plus(uuv)
}
height: width
radius: width / 2
x: offset * position.x - width / 2
y: offset * -position.y - height / 2
z: position.z
HoverHandler {
id: subBallHoverHandler
}
TapHandler {
acceptedButtons: Qt.LeftButton
onTapped: (eventPoint, button)=>{
subBallRoot.tapped()
//eventPoint.accepted = true
}
}
Text {
id: label
anchors.centerIn: parent
}
}
SubBall {
id: positiveX
targetNode: root.targetNode
width: ballBackground.subBallWidth
offset: ballBackground.subBallOffset
labelText: "X"
labelColor: hovered ? stylePalette.white : stylePalette.black
color: stylePalette.red
initialPosition: Qt.vector3d(1, 0, 0)
onTapped: {
root.axisClicked(OriginGizmo.Axis.PositiveX)
}
}
LineRectangle {
endPoint: Qt.vector2d(positiveX.x + ballBackground.subBallHalfWidth, positiveX.y + ballBackground.subBallHalfWidth)
color: stylePalette.red
z: positiveX.z - 0.001
}
SubBall {
id: positiveY
targetNode: root.targetNode
width: ballBackground.subBallWidth
offset: ballBackground.subBallOffset
labelText: "Y"
labelColor: hovered ? stylePalette.white : stylePalette.black
color: stylePalette.green
initialPosition: Qt.vector3d(0, 1, 0)
onTapped: {
root.axisClicked(OriginGizmo.Axis.PositiveY)
}
}
LineRectangle {
endPoint: Qt.vector2d(positiveY.x + ballBackground.subBallHalfWidth, positiveY.y + ballBackground.subBallHalfWidth)
color: stylePalette.green
z: positiveY.z - 0.001
}
SubBall {
id: positiveZ
targetNode: root.targetNode
width: ballBackground.subBallWidth
offset: ballBackground.subBallOffset
labelText: "Z"
labelColor: hovered ? stylePalette.white : stylePalette.black
color: stylePalette.blue
initialPosition: Qt.vector3d(0, 0, 1)
onTapped: {
root.axisClicked(OriginGizmo.Axis.PositiveZ)
}
}
LineRectangle {
endPoint: Qt.vector2d(positiveZ.x + ballBackground.subBallHalfWidth, positiveZ.y + ballBackground.subBallHalfWidth)
color: stylePalette.blue
z: positiveZ.z - 0.001
}
SubBall {
targetNode: root.targetNode
width: ballBackground.subBallWidth
offset: ballBackground.subBallOffset
labelText: "-X"
labelColor: stylePalette.white
labelVisible: hovered
color: Qt.rgba(stylePalette.red.r, stylePalette.red.g, stylePalette.red.b, z + 1 * 0.5)
border.color: stylePalette.red
border.width: 2
initialPosition: Qt.vector3d(-1, 0, 0)
onTapped: {
root.axisClicked(OriginGizmo.Axis.NegativeX)
}
}
SubBall {
targetNode: root.targetNode
width: ballBackground.subBallWidth
offset: ballBackground.subBallOffset
labelText: "-Y"
labelColor: stylePalette.white
labelVisible: hovered
color: Qt.rgba(stylePalette.green.r, stylePalette.green.g, stylePalette.green.b, z + 1 * 0.5)
border.color: stylePalette.green
border.width: 2
initialPosition: Qt.vector3d(0, -1, 0)
onTapped: {
root.axisClicked(OriginGizmo.Axis.NegativeY)
}
}
SubBall {
targetNode: root.targetNode
width: ballBackground.subBallWidth
offset: ballBackground.subBallOffset
labelText: "-Z"
labelColor: stylePalette.white
labelVisible: hovered
color: Qt.rgba(stylePalette.blue.r, stylePalette.blue.g, stylePalette.blue.b, z + 1 * 0.5)
border.color: stylePalette.blue
border.width: 2
initialPosition: Qt.vector3d(0, 0, -1)
onTapped: {
root.axisClicked(OriginGizmo.Axis.NegativeZ)
}
}
}
HoverHandler {
id: ballBackgroundHoverHandler
acceptedDevices: PointerDevice.Mouse
cursorShape: Qt.PointingHandCursor
}
DragHandler {
id: dragHandler
target: null
enabled: ballBackground.visible
onCentroidChanged: {
if (centroid.velocity.x > 0 && centroid.velocity.y > 0) {
root.ballMoved(centroid.velocity)
}
}
}
}
}
@@ -0,0 +1,86 @@
// Copyright (C) 2025 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import LightmapFile 1.0
Item {
id: root
implicitWidth: 360
implicitHeight: 420
ScrollView {
anchors.fill: parent
contentWidth: availableWidth
ColumnLayout {
anchors.fill: parent
spacing: 6
Label {
text: `Baked with Qt version: ${LightmapFile.qtVersion || "—"}`
font.bold: true
Layout.fillWidth: true
}
Label {
text: LightmapFile.bakeStart
visible: text.length > 0
Layout.fillWidth: true
wrapMode: Text.Wrap
Component.onCompleted: if (text.length)
text = "Bake initiated at:\n" + text
}
Label {
text: LightmapFile.bakeDuration
visible: text.length > 0
wrapMode: Text.Wrap
Layout.fillWidth: true
Component.onCompleted: if (text.length)
text = "Bake took:\n" + text
}
Label {
text: "Options used:"
Layout.fillWidth: true
}
ColumnLayout {
id: optionsColumn
Layout.fillWidth: true
spacing: 4
Repeater {
model: LightmapFile.options
delegate: RowLayout {
width: optionsColumn.width
spacing: 8
Label {
text: (modelData.key ?? "—") + ":"
font.bold: true
Layout.preferredWidth: 150
Layout.alignment: Qt.AlignRight | Qt.AlignTop
elide: Text.ElideRight
}
Label {
text: modelData.value !== undefined ? String(
modelData.value) : "—"
Layout.fillWidth: true
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
elide: Text.ElideRight
}
}
}
}
Item {
width: 1
height: 6
}
}
}
}
@@ -0,0 +1,50 @@
import QtQuick.tooling 1.2
// This file describes the plugin-supplied types contained in the library.
// It is used for QML tooling purposes only.
//
// This file was auto-generated by qmltyperegistrar.
Module {
Component {
file: "lightmapmesh.h"
lineNumber: 11
name: "LightmapMesh"
accessSemantics: "reference"
prototype: "QQuick3DGeometry"
exports: ["QtQuick3D.lightmapviewer/LightmapMesh 1.0"]
exportMetaObjectRevisions: [256]
Property {
name: "source"
type: "QUrl"
read: "source"
write: "setSource"
notify: "sourceChanged"
index: 0
lineNumber: 14
isFinal: true
}
Property {
name: "key"
type: "QString"
read: "key"
write: "setKey"
notify: "keyChanged"
index: 1
lineNumber: 15
isFinal: true
}
Property {
name: "bounds"
type: "QQuick3DBounds3"
read: "bounds"
notify: "boundsChanged"
index: 2
lineNumber: 16
isReadonly: true
}
Signal { name: "sourceChanged"; lineNumber: 30 }
Signal { name: "keyChanged"; lineNumber: 31 }
Signal { name: "boundsChanged"; lineNumber: 32 }
}
}
@@ -0,0 +1,12 @@
module QtQuick3D.lightmapviewer
typeinfo lightmapviewer.qmltypes
import QtQuick3D
prefer :/qt-project.org/imports/QtQuick3D/lightmapviewer/
LightmapViewer 1.0 LightmapViewer.qml
SceneMetadataView 1.0 SceneMetadataView.qml
MeshViewer 1.0 MeshViewer.qml
ImageViewer 1.0 ImageViewer.qml
ArcballController 1.0 ArcballController.qml
OriginGizmo 1.0 OriginGizmo.qml
depends QtQuick