From ca6dfa8cf9a30d2ee66591d6f699d357fb8c2d21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Meslin?= Date: Fri, 26 Jun 2026 17:51:56 +0200 Subject: [PATCH 1/3] Add Rigger: bones animation editor for DIE Rig model/primitives (Joint, Bone, Rig, .rig load/save) in common/engine, plus the Qt animator app: cylinder-view editor widget with joint editing, rectangle selection, joint and viewer property panels, and File/Help menus. --- animator/.gitignore | 2 + animator/CMakeLists.txt | 51 ++ animator/main.cpp | 68 +++ animator/mainwindow.cpp | 296 ++++++++++++ animator/mainwindow.h | 55 +++ animator/mainwindow.ui | 943 +++++++++++++++++++++++++++++++++++++ animator/rigger.cpp | 324 +++++++++++++ animator/rigger.h | 104 ++++ animator/rigger.pro | 54 +++ animator/wdgrigeditor.cpp | 281 +++++++++++ animator/wdgrigeditor.h | 58 +++ common/engine/rig.cpp | 186 ++++++++ common/engine/rig.h | 93 ++++ common/engine/rig_io.cpp | 183 +++++++ common/engine/rigobjects.h | 63 +++ 15 files changed, 2761 insertions(+) create mode 100644 animator/.gitignore create mode 100644 animator/CMakeLists.txt create mode 100644 animator/main.cpp create mode 100644 animator/mainwindow.cpp create mode 100644 animator/mainwindow.h create mode 100644 animator/mainwindow.ui create mode 100644 animator/rigger.cpp create mode 100644 animator/rigger.h create mode 100644 animator/rigger.pro create mode 100644 animator/wdgrigeditor.cpp create mode 100644 animator/wdgrigeditor.h create mode 100644 common/engine/rig.cpp create mode 100644 common/engine/rig.h create mode 100644 common/engine/rig_io.cpp create mode 100644 common/engine/rigobjects.h diff --git a/animator/.gitignore b/animator/.gitignore new file mode 100644 index 0000000..1ab240b --- /dev/null +++ b/animator/.gitignore @@ -0,0 +1,2 @@ +rigger.pro.* +rigger.pro.user diff --git a/animator/CMakeLists.txt b/animator/CMakeLists.txt new file mode 100644 index 0000000..e63bad1 --- /dev/null +++ b/animator/CMakeLists.txt @@ -0,0 +1,51 @@ +cmake_minimum_required(VERSION 3.16) +project(rigger LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTORCC ON) + +find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets) + +set(SOURCES + main.cpp + rigger.cpp + mainwindow.cpp + wdgrigeditor.cpp + ../common/engine/rig.cpp + ../common/engine/rig_io.cpp + mainwindow.ui +) + +#if(WIN32) +# list(APPEND SOURCES rigger.rc) +#endif() + +qt_add_executable(rigger WIN32 ${SOURCES}) + +target_include_directories(rigger PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} + ../common/engine +) + +target_link_libraries(rigger PRIVATE + Qt6::Core + Qt6::Gui + Qt6::Widgets +) + +#if(WIN32) +# target_link_libraries(rigger PRIVATE xinput winmm ws2_32) +#endif() + +if(MSVC) + target_compile_options(rigger PRIVATE /W4 /MP /WX-) + target_compile_options(rigger PRIVATE $<$:/O2>) +else() + target_compile_options(rigger PRIVATE -msse4 -save-temps -Wall -Wextra) + target_compile_options(rigger PRIVATE $<$:-O2>) + target_link_options(rigger PRIVATE $<$:-s>) +endif() diff --git a/animator/main.cpp b/animator/main.cpp new file mode 100644 index 0000000..718aede --- /dev/null +++ b/animator/main.cpp @@ -0,0 +1,68 @@ +/** + RIGGER + Animation editor for the DIE engine + (c) Fred's Lab 2024-2026 + Frédéric Meslin / info@fredslab.net + SPDX-License-Identifier: MIT + If used commercially, contributions, donations are highly appreciated. + + rigger entry point +*/ + +#include "mainwindow.h" + +#include "rigger.h" + +#include +#include + +#include +#include +#ifdef _MSC_VER +#include +#else +#include +#endif + +/*****************************************************************************/ +inline bool hasSSE41() +{ +#ifdef _MSC_VER + int cpuInfo[4]; + __cpuid(cpuInfo, 1); + return (cpuInfo[2] & (1 << 19)) != 0; // SSE4.1 is bit 19 of ECX +#else + uint32_t eax, ebx, ecx, edx; + __cpuid(1, eax, ebx, ecx, edx); + return (ecx & (1 << 19)) != 0; // SSE4.1 is bit 19 of ECX +#endif +} + +/*****************************************************************************/ +MainWindow * mainWindow = nullptr; + +int main(int argc, char *argv[]) +{ + QApplication a(argc, argv); + if (!hasSSE41()) { + QMessageBox::critical(nullptr, "Rigger", "This computer does not support SSE4.1!"); + return -1; + } + + QApplication::setWindowIcon(QIcon(":/rigger-icon.png")); + QApplication::setStyle("windows"); + + rigger.init(); + + mainWindow = new MainWindow(); + mainWindow->show(); + + int result = a.exec(); + + delete mainWindow; + mainWindow = nullptr; + + rigger.terminate(); + + return result; +} diff --git a/animator/mainwindow.cpp b/animator/mainwindow.cpp new file mode 100644 index 0000000..bc84997 --- /dev/null +++ b/animator/mainwindow.cpp @@ -0,0 +1,296 @@ +#include "mainwindow.h" +#include "ui_mainwindow.h" + +#include "rigger.h" + +#include +#include +#include +#include +#include +#include +#include + +MainWindow::MainWindow(QWidget *parent) + : QMainWindow(parent) + , ui(new Ui::MainWindow) +{ + ui->setupUi(this); + resizeUI(); + updateJointProperties(); + updateViewerProperties(); +} + +MainWindow::~MainWindow() +{ + delete ui; +} + +/*****************************************************************************/ +void MainWindow::resizeEvent(QResizeEvent *) +{ + resizeUI(); + update(); +} + +void MainWindow::resizeUI() +{ + int cw = ui->centralwidget->width(); + int ch = ui->centralwidget->height(); + + const int margin = 9; + const int leftW = 201; // tools tab + const int rightW = 151; // right-hand column + const int framesH = 64; + const int scrollH = 16; + +// Left tools tab, full height + ui->tabRiggerModes->setGeometry(0, 0, leftW, ch - 8); + +// Right column, anchored to the right edge (kept stacked as in the .ui) + int rightX = cw - rightW - 8; + ui->groupViewer->setGeometry(rightX, 0, rightW, 121); + ui->groupBox->setGeometry(rightX, 129, rightW, 81); + ui->groupAnimate->setGeometry(rightX, 220, rightW, 151); + ui->comboAnimationName->setGeometry(rightX, 380, rightW, 22); + +// Frame buttons (2x2), anchored to the bottom, left of the right column + const int btnW = 71, btnH = 23, gap = 8, rowGap = 7; + int blockW = 2 * btnW + gap; + int bottomH = framesH + 4 + scrollH; + int framesTop = ch - margin - bottomH; + + int blockX = rightX - margin - blockW; + ui->pushFrameAdd->setGeometry(blockX, framesTop, btnW, btnH); + ui->pushFrameDel->setGeometry(blockX, framesTop + btnH + rowGap, btnW, btnH); + ui->pushFrameCopy->setGeometry(blockX + btnW + gap, framesTop, btnW, btnH); + ui->pushFramePaste->setGeometry(blockX + btnW + gap, framesTop + btnH + rowGap, btnW, btnH); + +// Frames timeline + scrollbar fill the rest of the bottom strip + int framesX = leftW + margin; + int framesW = blockX - margin - framesX; + if (framesW < 1) framesW = 1; + ui->widgetFrames->setGeometry(framesX, framesTop, framesW, framesH); + ui->scrollFrames->setGeometry(framesX, framesTop + framesH + 4, framesW, scrollH); + +// Central canvas fills the area between the panels + int canvasX = leftW + margin; + int canvasY = margin; + int canvasW = rightX - margin - canvasX; + int canvasH = framesTop - margin - canvasY; + if (canvasW < 1) canvasW = 1; + if (canvasH < 1) canvasH = 1; + ui->widgetRig->setGeometry(canvasX, canvasY, canvasW, canvasH); +} + +/*****************************************************************************/ +void MainWindow::setSpinValueSilently(QAbstractSpinBox * box, double value) +{ + if (auto * doubleSpin = qobject_cast(box)) { + doubleSpin->blockSignals(true); + doubleSpin->setValue(value); + doubleSpin->blockSignals(false); + + } else if (auto * intSpin = qobject_cast(box)) { + intSpin->blockSignals(true); + intSpin->setValue(static_cast(value)); + intSpin->blockSignals(false); + } +} + +/*****************************************************************************/ +void MainWindow::updateJointProperties() +{ + Frame * cur = rigger.rig.currentFramePtr(); + int count = cur ? cur->joints.count() : 0; + +// Rebuild the list when the joint count changes + if (ui->listJoints->count() != count) { + ui->listJoints->blockSignals(true); + ui->listJoints->clear(); + for (int i = 0; i < count; i++) + ui->listJoints->addItem(QString::asprintf("Joint %04X", (unsigned int) i)); + ui->listJoints->blockSignals(false); + } + +// Mirror the per-joint selection into the list + ui->listJoints->blockSignals(true); + for (int i = 0; i < count; i++) + ui->listJoints->item(i)->setSelected(cur->joints[i].selected); + ui->listJoints->blockSignals(false); + + if (rigger.selectedJoint < 0 || rigger.selectedJoint >= count) { + ui->plainJointID->setPlainText("None"); + setSpinValueSilently(ui->spinJointX, 0.0); + setSpinValueSilently(ui->spinJointY, 0.0); + setSpinValueSilently(ui->spinJointZ, 0.0); + return; + } + + Joint & j = cur->joints[rigger.selectedJoint]; + ui->plainJointID->setPlainText(QString::number(rigger.selectedJoint)); + setSpinValueSilently(ui->spinJointX, j.pos.x()); + setSpinValueSilently(ui->spinJointY, j.pos.y()); + setSpinValueSilently(ui->spinJointZ, j.pos.z()); +} + +/*****************************************************************************/ +void MainWindow::on_spinJointX_valueChanged(double arg1) +{ + Frame * cur = rigger.rig.currentFramePtr(); + if (!cur || rigger.selectedJoint < 0) return; + for (Joint & j : cur->joints) { + if (!j.selected) continue; + j.pos.setX(arg1); + j.apos.setX(arg1); + } + ui->widgetRig->update(); +} + +void MainWindow::on_spinJointY_valueChanged(double arg1) +{ + Frame * cur = rigger.rig.currentFramePtr(); + if (!cur || rigger.selectedJoint < 0) return; + for (Joint & j : cur->joints) { + if (!j.selected) continue; + j.pos.setY(arg1); + j.apos.setY(arg1); + } + ui->widgetRig->update(); +} + +void MainWindow::on_spinJointZ_valueChanged(double arg1) +{ + Frame * cur = rigger.rig.currentFramePtr(); + if (!cur || rigger.selectedJoint < 0) return; + for (Joint & j : cur->joints) { + if (!j.selected) continue; + j.pos.setZ(arg1); + j.apos.setZ(arg1); + } + ui->widgetRig->update(); +} + +void MainWindow::on_listJoints_itemSelectionChanged() +{ + Frame * cur = rigger.rig.currentFramePtr(); + if (!cur) return; + + rigger.jointDeselectAll(); + rigger.selectedJoint = RIG_UNSELECTED; + + for (int row = 0; row < ui->listJoints->count() && row < cur->joints.count(); row++) { + if (!ui->listJoints->item(row)->isSelected()) continue; + cur->joints[row].selected = true; + rigger.selectedJoint = row; + } + + int current = ui->listJoints->currentRow(); + if (current >= 0 && current < cur->joints.count() && + ui->listJoints->item(current)->isSelected()) + rigger.selectedJoint = current; + + updateJointProperties(); + ui->widgetRig->update(); +} + +void MainWindow::on_pushJointDelete_clicked() +{ + if (rigger.selectedJoint < 0) return; + rigger.jointDelete(rigger.selectedJoint); + updateJointProperties(); + ui->widgetRig->update(); +} + +/*****************************************************************************/ +void MainWindow::updateViewerProperties() +{ + setSpinValueSilently(ui->spinViewerPan, rigger.rigView.pan); + setSpinValueSilently(ui->spinViewerY, rigger.rigView.y); + setSpinValueSilently(ui->spinViewerZ, rigger.rigView.diameter); +} + +void MainWindow::on_spinViewerPan_valueChanged(double arg1) +{ + rigger.rigView.pan = arg1; + ui->widgetRig->update(); +} + +void MainWindow::on_spinViewerY_valueChanged(double arg1) +{ + rigger.rigView.y = arg1; + ui->widgetRig->update(); +} + +void MainWindow::on_spinViewerZ_valueChanged(double arg1) +{ + rigger.rigView.diameter = arg1; + ui->widgetRig->update(); +} + +/*****************************************************************************/ +void MainWindow::on_actionNew_triggered() +{ + rigger.init(); + updateJointProperties(); + updateViewerProperties(); + ui->widgetRig->update(); + setWindowTitle("Rigger"); +} + +void MainWindow::on_actionLoad_triggered() +{ + QString path = rigger.rig.path.isEmpty() ? QDir::currentPath() : rigger.rig.path; + QString file = QFileDialog::getOpenFileName(this, "Open rig", path, "Rig File (*.rig)"); + if (file.isEmpty()) return; + + rigger.rig.load(file); + rigger.deselect(); + updateJointProperties(); + ui->widgetRig->update(); + setWindowTitle("Rigger : " + QFileInfo(file).fileName()); +} + +void MainWindow::on_actionSave_triggered() +{ + QString path = rigger.rig.path.isEmpty() ? QDir::currentPath() : rigger.rig.path; + QString file = QFileDialog::getSaveFileName(this, "Save rig", path, "Rig File (*.rig)"); + if (file.isEmpty()) return; + if (!file.endsWith(".rig", Qt::CaseInsensitive)) file += ".rig"; + + rigger.rig.save(file); + setWindowTitle("Rigger : " + QFileInfo(file).fileName()); +} + +void MainWindow::on_actionQuit_triggered() +{ + close(); +} + +/*****************************************************************************/ +void MainWindow::on_actionAbout_triggered() +{ + QString aboutText; + aboutText += "Rigger
"; + aboutText += "Animation editor for the DIE Engine

"; + aboutText += "© 2024–2026 Frédéric Meslin
"; + aboutText += "Fred's Lab
"; + aboutText += "info@fredslab.net

"; + aboutText += "Open Source under the MIT license.
"; + aboutText += "If used commercially, contributions and donations are highly appreciated.

"; + aboutText += "Rigger is an open-source character animation tool developed by Fred's Lab,
" + "for building animated sprites for DIE (Depth Integration Engine)."; + + QMessageBox aboutBox(this); + aboutBox.setWindowTitle("About Rigger"); + aboutBox.setTextFormat(Qt::RichText); + aboutBox.setTextInteractionFlags(Qt::TextBrowserInteraction); + aboutBox.setIcon(QMessageBox::Information); + aboutBox.setText(aboutText); + aboutBox.exec(); +} + +void MainWindow::on_actionAboutQt_triggered() +{ + QMessageBox::aboutQt(this, "About Qt"); +} diff --git a/animator/mainwindow.h b/animator/mainwindow.h new file mode 100644 index 0000000..0fd43c9 --- /dev/null +++ b/animator/mainwindow.h @@ -0,0 +1,55 @@ +#ifndef MAINWINDOW_H +#define MAINWINDOW_H + +#include +#include + +namespace Ui { +class MainWindow; +} + +class MainWindow : public QMainWindow +{ + Q_OBJECT + +public: + explicit MainWindow(QWidget *parent = nullptr); + ~MainWindow(); + + /// \brief Refresh the joint list and the selected joint's properties + void updateJointProperties(); + + /// \brief Refresh the viewer controls from the rig view + void updateViewerProperties(); + +protected: + void resizeEvent(QResizeEvent *event) override; + +private: + void resizeUI(); + void setSpinValueSilently(QAbstractSpinBox * box, double value); + + Ui::MainWindow *ui; + +private slots: + void on_spinJointX_valueChanged(double arg1); + void on_spinJointY_valueChanged(double arg1); + void on_spinJointZ_valueChanged(double arg1); + void on_listJoints_itemSelectionChanged(); + void on_pushJointDelete_clicked(); + + void on_spinViewerPan_valueChanged(double arg1); + void on_spinViewerY_valueChanged(double arg1); + void on_spinViewerZ_valueChanged(double arg1); + + void on_actionNew_triggered(); + void on_actionLoad_triggered(); + void on_actionSave_triggered(); + void on_actionQuit_triggered(); + void on_actionAbout_triggered(); + void on_actionAboutQt_triggered(); +}; + +extern MainWindow * mainWindow; + +#endif // MAINWINDOW_H diff --git a/animator/mainwindow.ui b/animator/mainwindow.ui new file mode 100644 index 0000000..eb35925 --- /dev/null +++ b/animator/mainwindow.ui @@ -0,0 +1,943 @@ + + + MainWindow + + + + 0 + 0 + 900 + 490 + + + + + 900 + 490 + + + + Rigger + + + + + + 0 + 0 + 201 + 451 + + + + QTabWidget::TabPosition::West + + + QTabWidget::TabShape::Rounded + + + 1 + + + + + :/Icons/tools-node.png:/Icons/tools-node.png + + + Joints + + + + + 10 + 10 + 61 + 21 + + + + ID: + + + + + + 100 + 130 + 61 + 23 + + + + Delete + + + + + + 10 + 70 + 61 + 21 + + + + Y: + + + + + + 70 + 70 + 91 + 22 + + + + -1024.000000000000000 + + + 1024.000000000000000 + + + 0.250000000000000 + + + + + + 70 + 40 + 91 + 22 + + + + -1024.000000000000000 + + + 1024.000000000000000 + + + 0.250000000000000 + + + + + + 70 + 10 + 91 + 21 + + + + Qt::ScrollBarPolicy::ScrollBarAlwaysOff + + + Qt::ScrollBarPolicy::ScrollBarAlwaysOff + + + false + + + QPlainTextEdit::LineWrapMode::NoWrap + + + true + + + + + + 70 + 100 + 91 + 22 + + + + -1024.000000000000000 + + + 1024.000000000000000 + + + 0.250000000000000 + + + + + + 10 + 100 + 61 + 21 + + + + Z: + + + + + + 10 + 40 + 61 + 21 + + + + X: + + + + + + 10 + 200 + 151 + 271 + + + + QAbstractItemView::SelectionMode::ExtendedSelection + + + + + + 10 + 175 + 151 + 21 + + + + Joints: + + + + + + + :/Icons/tools-wall.png:/Icons/tools-wall.png + + + Bones + + + + + 10 + 10 + 61 + 21 + + + + ID: + + + + + + 11 + 220 + 151 + 22 + + + + + Front + + + + + Right + + + + + Back + + + + + Left + + + + + + + 50 + 250 + 64 + 64 + + + + + + + 100 + 410 + 61 + 23 + + + + Delete + + + + + + 10 + 160 + 61 + 16 + + + + Props: + + + + + + 70 + 160 + 91 + 20 + + + + invisible + + + + + + 70 + 10 + 91 + 21 + + + + Qt::ScrollBarPolicy::ScrollBarAlwaysOff + + + Qt::ScrollBarPolicy::ScrollBarAlwaysOff + + + false + + + QPlainTextEdit::LineWrapMode::NoWrap + + + true + + + + + + 11 + 320 + 151 + 22 + + + + 0 + + + 1024 + + + + + + 70 + 70 + 91 + 22 + + + + -1024.000000000000000 + + + 1024.000000000000000 + + + 0.250000000000000 + + + + + + 10 + 40 + 61 + 21 + + + + Width: + + + + + + 70 + 40 + 91 + 22 + + + + -1024.000000000000000 + + + 1024.000000000000000 + + + 0.250000000000000 + + + + + + 10 + 70 + 61 + 21 + + + + Length: + + + + + + 70 + 100 + 91 + 22 + + + + -1024.000000000000000 + + + 1024.000000000000000 + + + 0.250000000000000 + + + + + + 10 + 100 + 61 + 21 + + + + Offset: + + + + + + + :/Icons/tools-settings.png:/Icons/tools-settings.png + + + Config + + + + + 20 + 40 + 131 + 20 + + + + joints + + + + + + 10 + 10 + 151 + 16 + + + + Display: + + + + + + 20 + 60 + 131 + 20 + + + + bones + + + + + + 20 + 80 + 131 + 20 + + + + flesh + + + + + + + + 210 + 0 + 521 + 351 + + + + + + + 210 + 360 + 361 + 64 + + + + + + + 580 + 360 + 71 + 23 + + + + Add + + + + + + 580 + 390 + 71 + 23 + + + + Del + + + + + + 660 + 360 + 71 + 23 + + + + Copy + + + + + + 660 + 390 + 71 + 23 + + + + Paste + + + + + + 740 + 0 + 151 + 121 + + + + Viewer: + + + + + 10 + 60 + 51 + 21 + + + + Z: + + + + + + 10 + 30 + 51 + 21 + + + + Y: + + + + + + 10 + 90 + 51 + 21 + + + + Pan: + + + + + + 60 + 30 + 81 + 22 + + + + -1024.000000000000000 + + + 1024.000000000000000 + + + + + + 60 + 60 + 81 + 22 + + + + -1024.000000000000000 + + + 1024.000000000000000 + + + + + + 60 + 90 + 81 + 22 + + + + -1024.000000000000000 + + + 1024.000000000000000 + + + + + + + 740 + 220 + 151 + 151 + + + + Animate: + + + + + 10 + 60 + 51 + 21 + + + + Stop: + + + + + + 10 + 30 + 51 + 21 + + + + Start: + + + + + + 10 + 90 + 51 + 21 + + + + Speed: + + + + + + 60 + 90 + 81 + 22 + + + + -1024.000000000000000 + + + 1024.000000000000000 + + + + + + 60 + 30 + 81 + 22 + + + + + + + 60 + 60 + 81 + 22 + + + + + + + 10 + 120 + 131 + 21 + + + + Run + + + + + + + 210 + 430 + 361 + 16 + + + + Qt::Orientation::Horizontal + + + + + + 740 + 380 + 151 + 22 + + + + + Default + + + + + + + 740 + 129 + 151 + 81 + + + + Editor: + + + + + 10 + 30 + 131 + 21 + + + + All frames + + + true + + + true + + + false + + + + + + + + 0 + 0 + 900 + 25 + + + + + File + + + + + + + + + + + Help + + + + + + + + + + New + + + Ctrl+N + + + + + Load ... + + + Ctrl+O + + + + + Save ... + + + Ctrl+S + + + + + Quit + + + Ctrl+Q + + + + + About Rigger + + + + + About Qt + + + + + + WdgRigEditor + QWidget +
wdgrigeditor.h
+ 1 +
+
+ + +
diff --git a/animator/rigger.cpp b/animator/rigger.cpp new file mode 100644 index 0000000..d3197ac --- /dev/null +++ b/animator/rigger.cpp @@ -0,0 +1,324 @@ +/** + RIGGER + Animation editor for the DIE engine + (c) Fred's Lab 2024-2026 + Frédéric Meslin / info@fredslab.net + SPDX-License-Identifier: MIT + If used commercially, contributions, donations are highly appreciated. + + rigger model +*/ + +#include "rigger.h" + +#include +#include +#include +#include + +static constexpr float D2R = 3.14159265f / 180.0f; + +Rigger rigger; + +/*****************************************************************************/ +Rigger::Rigger() +{ +} + +/*****************************************************************************/ +void Rigger::init() +{ + rig.init(); + rig.animationAdd("idle"); // a default animation with one frame, ready to edit + + rigMode = RIG_MODE_JOINTS; + rigView = { 0.0f, 32.0f, 0.0f }; + + deselect(); +} + +void Rigger::terminate() +{ + rig.terminate(); +} + +/*****************************************************************************/ +void Rigger::selectAll() +{ + switch (rigMode) { + case RIG_MODE_JOINTS: jointSelectAll(); break; + case RIG_MODE_BONES: boneSelectAll(); break; + default: break; + } +} + +void Rigger::deselect() +{ + selectedJoint = RIG_UNSELECTED; + selectedBone = RIG_UNSELECTED; + jointDeselectAll(); + boneDeselectAll(); +} + +/*****************************************************************************/ +void Rigger::cut() +{ + +} + +void Rigger::copy() +{ + +} + +void Rigger::paste(QVector2D pos) +{ + (void) pos; +} + +/*****************************************************************************/ +void Rigger::jointSelect(int jId) +{ + Frame * cur = rig.currentFramePtr(); + if (!cur) return; + if (jId < 0 || jId >= cur->joints.count()) return; + + cur->joints[jId].selected = true; + selectedJoint = jId; + rigMode = RIG_MODE_JOINTS; +} + +bool Rigger::jointAdd(QVector3D pos, int & jId) +{ + Frame * cur = rig.currentFramePtr(); + if (!cur) return false; + + jId = cur->joints.count(); + + Joint j{}; + j.pos = pos; + j.apos = pos; + j.flags = JOINT_FLAG_USED; + +// A joint belongs to the skeleton topology: add it to every frame so bones, +// which index joints, stay valid no matter which frame is edited. Only the +// current frame's copy is selected. + for (Animation & a : rig.animations) + for (Frame & f : a.frames) { + Joint nj = j; + nj.selected = (&f == cur); + f.joints.append(nj); + } + + selectedJoint = jId; + rigMode = RIG_MODE_JOINTS; + return true; +} + +void Rigger::jointDelete(int jId) +{ + Frame * cur = rig.currentFramePtr(); + if (!cur) return; + if (jId < 0 || jId >= cur->joints.count()) return; + +// Remove the joint from every frame to keep the topology consistent + for (Animation & a : rig.animations) + for (Frame & f : a.frames) + if (jId < f.joints.count()) + f.joints.removeAt(jId); + +// Drop the bones touching the joint, reindex those past it + for (int i = 0; i < rig.bones.count(); i++) { + Bone & b = rig.bones[i]; + if (b.jointID1 == jId || b.jointID2 == jId) { + boneDelete(i--); + continue; + } + if (b.jointID1 > jId) b.jointID1--; + if (b.jointID2 > jId) b.jointID2--; + } + + if (selectedJoint == jId) selectedJoint = RIG_UNSELECTED; + else if (selectedJoint > jId) selectedJoint--; +} + +void Rigger::jointSelectAll() +{ + Frame * cur = rig.currentFramePtr(); + if (!cur) return; + for (Joint & j : cur->joints) j.selected = true; +} + +void Rigger::jointDeselectAll() +{ + Frame * cur = rig.currentFramePtr(); + if (!cur) return; + for (Joint & j : cur->joints) j.selected = false; +} + +bool Rigger::jointFindInCircle(QVector2D pos, int & jId) +{ + jId = RIG_UNSELECTED; + Frame * cur = rig.currentFramePtr(); + if (!cur) return false; + + int count = cur->joints.count(); + if (count == 0) return false; + + int offset = selectedJoint < 0 ? 0 : selectedJoint + 1; + float rMin = RIGGER_JOINT_RADIUS / zoom(); + + for (int i = 0; i < count; i++) { + int index = (i + offset) % count; + QVector2D jp = to2D(cur->joints[index].pos); + float r = jp.distanceToPoint(pos); + if (r > rMin) continue; + rMin = r; + jId = index; + break; + } + + return jId != RIG_UNSELECTED; +} + +bool Rigger::jointFindInRect(QVector2D c1, QVector2D c2, int & jId) +{ + jId = RIG_UNSELECTED; + Frame * cur = rig.currentFramePtr(); + if (!cur) return false; + + for (int i = 0; i < cur->joints.count(); i++) { + QVector2D jp = to2D(cur->joints[i].pos); + QVector2D d1 = c1 - jp; + QVector2D d2 = c2 - jp; + if (d1.x() * d2.x() > 0.0f) continue; + if (d1.y() * d2.y() > 0.0f) continue; + cur->joints[i].selected = true; + jId = i; + } + + return jId != RIG_UNSELECTED; +} + +/*****************************************************************************/ +void Rigger::boneSelect(int bId) +{ + if (bId < 0 || bId >= rig.bones.count()) return; + rig.bones[bId].selected = true; + selectedBone = bId; + rigMode = RIG_MODE_BONES; +} + +bool Rigger::boneAdd(int j1, int j2, int & bId) +{ + Frame * cur = rig.currentFramePtr(); + if (!cur) return false; + if (j1 == j2) return false; + if (j1 < 0 || j1 >= cur->joints.count()) return false; + if (j2 < 0 || j2 >= cur->joints.count()) return false; + + Bone b{}; + b.jointID1 = (uint16_t) j1; + b.jointID2 = (uint16_t) j2; + b.width = 8.0f; + b.length = 0.0f; + b.offset = 0.0f; + b.imageCount = 0; + b.selected = true; + + rig.bones.append(b); + bId = rig.bones.count() - 1; + return true; +} + +void Rigger::boneDelete(int bId) +{ + if (bId < 0 || bId >= rig.bones.count()) return; + rig.bones.removeAt(bId); + + if (selectedBone == bId) selectedBone = RIG_UNSELECTED; + else if (selectedBone > bId) selectedBone--; +} + +void Rigger::boneSelectAll() +{ + for (Bone & b : rig.bones) b.selected = true; +} + +void Rigger::boneDeselectAll() +{ + for (Bone & b : rig.bones) b.selected = false; +} + +bool Rigger::boneFindInCircle(QVector2D pos, int & bId) +{ + bId = RIG_UNSELECTED; + Frame * cur = rig.currentFramePtr(); + if (!cur) return false; + + int count = rig.bones.count(); + if (count == 0) return false; + + int offset = selectedBone < 0 ? 0 : selectedBone + 1; + float rMin = RIGGER_BONE_RADIUS / zoom(); + + for (int i = 0; i < count; i++) { + int index = (i + offset) % count; + const Bone & b = rig.bones[index]; + if (b.jointID1 >= cur->joints.count()) continue; + if (b.jointID2 >= cur->joints.count()) continue; + + QVector2D p1 = to2D(cur->joints[b.jointID1].pos); + QVector2D p2 = to2D(cur->joints[b.jointID2].pos); + + // distance from the cursor to the bone segment + QVector2D dir = p2 - p1; + float len2 = QVector2D::dotProduct(dir, dir); + float t = len2 > 0.0f ? QVector2D::dotProduct(pos - p1, dir) / len2 : 0.0f; + t = std::clamp(t, 0.0f, 1.0f); + float r = (p1 + dir * t).distanceToPoint(pos); + if (r > rMin) continue; + + rMin = r; + bId = index; + break; + } + + return bId != RIG_UNSELECTED; +} + +bool Rigger::boneFindInRect(QVector2D c1, QVector2D c2, int & bId) +{ + bId = RIG_UNSELECTED; + Frame * cur = rig.currentFramePtr(); + if (!cur) return false; + + for (int i = 0; i < rig.bones.count(); i++) { + Bone & b = rig.bones[i]; + if (b.jointID1 >= cur->joints.count()) continue; + if (b.jointID2 >= cur->joints.count()) continue; + + QVector2D p1 = to2D(cur->joints[b.jointID1].pos); + QVector2D d11 = c1 - p1, d12 = c2 - p1; + if (d11.x() * d12.x() > 0.0f) continue; + if (d11.y() * d12.y() > 0.0f) continue; + + QVector2D p2 = to2D(cur->joints[b.jointID2].pos); + QVector2D d21 = c1 - p2, d22 = c2 - p2; + if (d21.x() * d22.x() > 0.0f) continue; + if (d21.y() * d22.y() > 0.0f) continue; + + b.selected = true; + bId = i; + } + + return bId != RIG_UNSELECTED; +} + +/*****************************************************************************/ +QVector2D Rigger::to2D(const QVector3D & pos) const +{ + float a = rigView.pan * D2R; + float x = pos.x() * cosf(a) + pos.z() * sinf(a); + return QVector2D(x, -pos.y()); +} \ No newline at end of file diff --git a/animator/rigger.h b/animator/rigger.h new file mode 100644 index 0000000..52c993a --- /dev/null +++ b/animator/rigger.h @@ -0,0 +1,104 @@ +/** + RIGGER + Animation editor for the DIE engine + (c) Fred's Lab 2024-2026 + Frédéric Meslin / info@fredslab.net + SPDX-License-Identifier: MIT + If used commercially, contributions, donations are highly appreciated. + + rigger model +*/ + +#ifndef RIGGER_H +#define RIGGER_H + +#include "rigobjects.h" +#include "rig.h" +//#include "renderer.h" + +#include +#include +#include + +#include +#include + +typedef enum { + RIG_MODE_JOINTS = 0, + RIG_MODE_BONES, + RIG_MODE_CONFIG, +} RIG_MODES; + +/*****************************************************************************/ +constexpr int RIGGER_JOINT_RADIUS = 8; +constexpr int RIGGER_BONE_RADIUS = 16; + +/// \brief Pixels spanned by the view diameter (sets the world-to-screen scale) +constexpr float RIGGER_VIEW_SCALE = 256.0f; + +/** + \brief Orthographic cylinder view: the world rotated by pan about Y, seen + from a Z distance (diameter), with a vertical offset +*/ +struct RigView { + float pan; ///< rotation about the Y axis, in degrees + float diameter; ///< cylinder diameter / camera Z distance + float y; ///< vertical offset +}; + +/*****************************************************************************/ +class Rigger +{ +public: + Rigger(); + void init(); + void terminate(); + + RIG_MODES rigMode; + RigView rigView; + + /// \brief World-to-screen scale, derived from the view diameter + float zoom() const { + return rigView.diameter > 0.0f ? RIGGER_VIEW_SCALE / rigView.diameter : 1.0f; + } + + Rig rig; + + int selectedJoint; + int selectedBone; + + //bool inView(const QVector3D & pos) const { + // return pos.y() >= viewMinY && pos.y() <= viewMaxY; + //} + + void selectAll(); + void deselect(); + + void cut(); + void copy(); + void paste(QVector2D pos); + +// Per-object operations. + void jointSelect(int jId); + bool jointAdd(QVector3D pos, int & jId); + void jointDelete(int jId); + void jointSelectAll(); + void jointDeselectAll(); + bool jointFindInCircle(QVector2D pos, int & jId); + bool jointFindInRect(QVector2D c1, QVector2D c2, int & jId); + + void boneSelect(int bId); + bool boneAdd(int j1, int j2, int & bId); + void boneDelete(int bId); + void boneSelectAll(); + void boneDeselectAll(); + bool boneFindInCircle(QVector2D pos, int & bId); + bool boneFindInRect(QVector2D c1, QVector2D c2, int & bId); + + /// \brief Project a world position onto the 2D cylinder view plane + QVector2D to2D(const QVector3D & pos) const; +}; + +extern Rigger rigger; + +#endif // RIGGER_H diff --git a/animator/rigger.pro b/animator/rigger.pro new file mode 100644 index 0000000..b183990 --- /dev/null +++ b/animator/rigger.pro @@ -0,0 +1,54 @@ +QT += core gui + +greaterThan(QT_MAJOR_VERSION, 4): QT += widgets + +CONFIG += c++17 + +win32-msvc* { + QMAKE_CXXFLAGS_RELEASE *= /O2 + QMAKE_CXXFLAGS += /W4 /MP +} else { + QMAKE_CXXFLAGS_RELEASE *= -O2 + QMAKE_LFLAGS_RELEASE *= -s + QMAKE_CXXFLAGS += -msse4 -save-temps -Wall -Wextra +} + +#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 + +INCLUDEPATH += \ + ../common/engine + +SOURCES += \ + main.cpp \ + mainwindow.cpp \ + rigger.cpp \ + wdgrigeditor.cpp \ + ../common/engine/rig.cpp \ + ../common/engine/rig_io.cpp + +HEADERS += \ + mainwindow.h \ + rigger.h \ + wdgrigeditor.h \ + ../common/engine/rig.h \ + ../common/engine/rigobjects.h + +FORMS += \ + mainwindow.ui + +#win32:LIBS += \ +# -lXinput \ +# -lwinmm \ +# -lws2_32 +# +#linux:LIBS += \ + +qnx: target.path = /tmp/$${TARGET}/bin +else: unix:!android: target.path = /opt/$${TARGET}/bin +!isEmpty(target.path): INSTALLS += target + +#RESOURCES += \ +# resources.qrc + +#RC_FILE += \ +# rigger.rc diff --git a/animator/wdgrigeditor.cpp b/animator/wdgrigeditor.cpp new file mode 100644 index 0000000..3e6e5a4 --- /dev/null +++ b/animator/wdgrigeditor.cpp @@ -0,0 +1,281 @@ +/** + RIGGER + Animation editor for the DIE engine + (c) Fred's Lab 2024-2026 + Frédéric Meslin / info@fredslab.net + SPDX-License-Identifier: MIT + If used commercially, contributions, donations are highly appreciated. + + 2D rig editor widget +*/ + +#include "wdgrigeditor.h" + +#include "rigger.h" +#include "mainwindow.h" + +#include +#include + +#include + +static constexpr float D2R = 3.14159265f / 180.0f; + +/*****************************************************************************/ +QPen WdgRigEditor::colorPrincipal = QColor(255, 255, 255); +QPen WdgRigEditor::colorSelected = QColor(192, 192, 192); +QPen WdgRigEditor::colorJointBase = QColor(192, 32, 32); +QPen WdgRigEditor::colorBoneBase = QColor(32, 32, 255); +QPen WdgRigEditor::colorAxis = QColor(48, 48, 48); + +/*****************************************************************************/ +WdgRigEditor::WdgRigEditor(QWidget * parent) : + QWidget(parent), + scroll(), + pressWorld(), + selectRegionC1(), selectRegionC2(), + selectRegion(false), + selectRegionStart(false), + mouseLeftWasPressed(false) +{ + setMouseTracking(true); +} + +/*****************************************************************************/ +QVector2D WdgRigEditor::origin() const +{ + return QVector2D(width() * 0.5f, height() * 0.5f + rigger.rigView.y * rigger.zoom()); +} + +QVector2D WdgRigEditor::getWorldCoordinates(const QVector2D & screen) const +{ + return (screen - origin()) / rigger.zoom(); +} + +/*****************************************************************************/ +void WdgRigEditor::drawAxes(QPainter & painter, const QVector2D & org) +{ + painter.setPen(colorAxis); + painter.drawLine(QPointF(org.x(), 0), QPointF(org.x(), height())); // Y axis + painter.drawLine(QPointF(0, org.y()), QPointF(width(), org.y())); // ground +} + +void WdgRigEditor::drawBones(QPainter & painter, const QVector2D & org) +{ + Frame * cur = rigger.rig.currentFramePtr(); + if (!cur) return; + float zoom = rigger.zoom(); + + painter.setRenderHint(QPainter::Antialiasing, true); + for (int i = 0; i < rigger.rig.bones.count(); i++) { + const Bone & b = rigger.rig.bones[i]; + if (b.jointID1 >= cur->joints.count()) continue; + if (b.jointID2 >= cur->joints.count()) continue; + + QVector2D p1 = org + rigger.to2D(cur->joints[b.jointID1].pos) * zoom; + QVector2D p2 = org + rigger.to2D(cur->joints[b.jointID2].pos) * zoom; + + if (i == rigger.selectedBone) painter.setPen(colorPrincipal); + else if (b.selected) painter.setPen(colorSelected); + else painter.setPen(colorBoneBase); + + painter.drawLine(p1.toPointF(), p2.toPointF()); + } + painter.setRenderHint(QPainter::Antialiasing, false); +} + +void WdgRigEditor::drawJoints(QPainter & painter, const QVector2D & org) +{ + Frame * cur = rigger.rig.currentFramePtr(); + if (!cur) return; + float zoom = rigger.zoom(); + + painter.setRenderHint(QPainter::Antialiasing, true); + for (int pass = 0; pass < 3; pass++) { + for (int i = 0; i < cur->joints.count(); i++) { + const Joint & j = cur->joints[i]; + + if (pass == 0) { if (j.selected || i == rigger.selectedJoint) continue; } + else if (pass == 1) { if (!j.selected) continue; } + else if (i != rigger.selectedJoint) continue; + + QVector2D jp = org + rigger.to2D(j.pos) * zoom; + + if (i == rigger.selectedJoint) painter.setPen(colorPrincipal); + else if (j.selected) painter.setPen(colorSelected); + else painter.setPen(colorJointBase); + + painter.setBrush(painter.pen().color()); + painter.drawEllipse(jp.toPointF(), 3.0, 3.0); + } + } + painter.setBrush(Qt::NoBrush); + painter.setRenderHint(QPainter::Antialiasing, false); +} + +/*****************************************************************************/ +void WdgRigEditor::paintEvent(QPaintEvent *) +{ + QPainter painter(this); + painter.fillRect(rect(), Qt::black); + + QVector2D org = origin(); + drawAxes(painter, org); + drawBones(painter, org); + drawJoints(painter, org); + + if (selectRegion) { + painter.setPen(Qt::white); + painter.setBrush(Qt::NoBrush); + int rw = selectRegionC2.x() - selectRegionC1.x(); + int rh = selectRegionC2.y() - selectRegionC1.y(); + painter.drawRect(selectRegionC1.x(), selectRegionC1.y(), rw, rh); + } +} + +/*****************************************************************************/ +void WdgRigEditor::mousePressEvent(QMouseEvent * event) +{ + QVector2D click(event->position()); + +// Middle button starts an orbit / vertical slide + if (event->buttons() & Qt::MiddleButton) { + scroll = click; + return; + } + if (!(event->buttons() & Qt::LeftButton)) return; + + mouseLeftWasPressed = true; + selectRegion = false; + selectRegionStart = true; + selectRegionC1 = selectRegionC2 = click; + + if (rigger.rigMode != RIG_MODE_JOINTS) return; + + bool shift = event->modifiers() & Qt::ShiftModifier; + QVector2D world = getWorldCoordinates(click); + pressWorld = world; + + int jId; + if (rigger.jointFindInCircle(world, jId)) { + // Hit a joint: select it (keep the group when shift / already selected) + Frame * cur = rigger.rig.currentFramePtr(); + bool already = cur && jId < cur->joints.count() && cur->joints[jId].selected; + if (!shift && !already) rigger.jointDeselectAll(); + selectRegionStart = false; + rigger.selectedJoint = jId; + if (cur && jId < cur->joints.count()) cur->joints[jId].selected = true; + + } else { + // Missed: clear the selection, a region or a create may follow + rigger.selectedJoint = RIG_UNSELECTED; + rigger.jointDeselectAll(); + } + + if (mainWindow) mainWindow->updateJointProperties(); + update(); +} + +void WdgRigEditor::mouseMoveEvent(QMouseEvent * event) +{ +// Middle-drag orbits the cylinder view + if (event->buttons() & Qt::MiddleButton) { + QVector2D p = QVector2D(event->position()); + QVector2D d = p - scroll; + scroll = p; + + rigger.rigView.pan += d.x() * 0.5f; + rigger.rigView.y += d.y() / rigger.zoom(); + if (mainWindow) mainWindow->updateViewerProperties(); + update(); + return; + } + + if (!(event->buttons() & Qt::LeftButton)) return; + if (rigger.rigMode != RIG_MODE_JOINTS) return; + + QVector2D click(event->position()); + +// Press landed on empty space: grow a selection rectangle + if (selectRegionStart) { + selectRegion = true; + selectRegionC2 = click; + update(); + return; + } + +// Press landed on a joint: drag the selection within the current viewing +// plane, preserving each joint's depth so it doesn't snap to the axis plane + Frame * cur = rigger.rig.currentFramePtr(); + if (!cur) return; + if (rigger.selectedJoint < 0 || rigger.selectedJoint >= cur->joints.count()) return; + + QVector2D world = getWorldCoordinates(click); + float a = rigger.rigView.pan * D2R; + float ca = cosf(a), sa = sinf(a); + + Joint & jp = cur->joints[rigger.selectedJoint]; + float depth = -jp.pos.x() * sa + jp.pos.z() * ca; + float vx = world.x(); + QVector3D target(vx * ca - depth * sa, -world.y(), vx * sa + depth * ca); + +// Translate every selected joint by the primary's delta (rigid group move) + QVector3D delta = target - jp.pos; + for (Joint & j : cur->joints) { + if (!j.selected) continue; + j.pos += delta; + j.apos = j.pos; + } + + if (mainWindow) mainWindow->updateJointProperties(); + update(); +} + +void WdgRigEditor::mouseReleaseEvent(QMouseEvent * event) +{ + if (mouseLeftWasPressed && rigger.rigMode == RIG_MODE_JOINTS) { + if (!selectRegion) { + // A plain click on empty space drops a new joint on the camera plane + if (rigger.selectedJoint == RIG_UNSELECTED) { + float a = rigger.rigView.pan * D2R; + QVector3D mark(pressWorld.x() * cosf(a), -pressWorld.y(), pressWorld.x() * sinf(a)); + rigger.jointDeselectAll(); + int jNew; + rigger.jointAdd(mark, jNew); + } + + } else { + // A dragged rectangle selects every joint inside it + bool shift = event->modifiers() & Qt::ShiftModifier; + if (!shift) rigger.jointDeselectAll(); + QVector2D c1 = getWorldCoordinates(selectRegionC1); + QVector2D c2 = getWorldCoordinates(selectRegionC2); + int jId; + if (rigger.jointFindInRect(c1, c2, jId)) + rigger.selectedJoint = jId; + } + + if (mainWindow) mainWindow->updateJointProperties(); + } + + selectRegion = false; + selectRegionStart = false; + mouseLeftWasPressed = false; + update(); +} + +void WdgRigEditor::wheelEvent(QWheelEvent * event) +{ + constexpr float diaMin = 1.0f; + constexpr float diaMax = 1024.0f; + + QPoint degrees = event->angleDelta(); + if (degrees.y() > 0) rigger.rigView.diameter *= 0.8f; // zoom in -> smaller view + if (degrees.y() < 0) rigger.rigView.diameter *= 1.25f; // zoom out -> larger view + if (rigger.rigView.diameter < diaMin) rigger.rigView.diameter = diaMin; + if (rigger.rigView.diameter > diaMax) rigger.rigView.diameter = diaMax; + + if (mainWindow) mainWindow->updateViewerProperties(); + event->accept(); + update(); +} diff --git a/animator/wdgrigeditor.h b/animator/wdgrigeditor.h new file mode 100644 index 0000000..8428c83 --- /dev/null +++ b/animator/wdgrigeditor.h @@ -0,0 +1,58 @@ +/** + RIGGER + Animation editor for the DIE engine + (c) Fred's Lab 2024-2026 + Frédéric Meslin / info@fredslab.net + SPDX-License-Identifier: MIT + If used commercially, contributions, donations are highly appreciated. + + 2D rig editor widget +*/ + +#ifndef WDG_RIGEDITOR_H +#define WDG_RIGEDITOR_H + +#include +#include +#include + +class WdgRigEditor : public QWidget +{ + Q_OBJECT + +public: + explicit WdgRigEditor(QWidget * parent = nullptr); + +protected: + void paintEvent(QPaintEvent * event) override; + + void mousePressEvent(QMouseEvent * event) override; + void mouseMoveEvent(QMouseEvent * event) override; + void mouseReleaseEvent(QMouseEvent * event) override; + void wheelEvent(QWheelEvent * event) override; + +private: + QVector2D scroll; ///< last cursor position during an orbit drag + + QVector2D pressWorld; ///< world cursor at the left press (used to create) + QVector2D selectRegionC1; + QVector2D selectRegionC2; + bool selectRegion; + bool selectRegionStart; + bool mouseLeftWasPressed; + + static QPen colorPrincipal; + static QPen colorSelected; + static QPen colorJointBase; + static QPen colorBoneBase; + static QPen colorAxis; + + QVector2D origin() const; + QVector2D getWorldCoordinates(const QVector2D & screen) const; + + void drawAxes(QPainter & painter, const QVector2D & org); + void drawBones(QPainter & painter, const QVector2D & org); + void drawJoints(QPainter & painter, const QVector2D & org); +}; + +#endif // WDG_RIGEDITOR_H diff --git a/common/engine/rig.cpp b/common/engine/rig.cpp new file mode 100644 index 0000000..8a2dd76 --- /dev/null +++ b/common/engine/rig.cpp @@ -0,0 +1,186 @@ +/** + DIE ENGINE + Depth Integration Engine / A modern ray-caster + (c) Fred's Lab 2024-2026 + Frédéric Meslin / info@fredslab.net + SPDX-License-Identifier: MIT + If used commercially, contributions, donations are highly appreciated. + + rig model +*/ + +#include "rig.h" + +#include +#include + +/*****************************************************************************/ +Rig::Rig() +{ + init(); +} + +/*****************************************************************************/ +void Rig::init() +{ + clearAnimations(); + bones.clear(); + path.clear(); + textures = QImage(); + + currentAnimation = RIG_UNSELECTED; + currentFrame = RIG_UNSELECTED; + playCursor = 0.0f; + playing = false; +} + +void Rig::terminate() +{ + clearAnimations(); + bones.clear(); +} + +void Rig::clearAnimations() +{ + animations.clear(); + currentAnimation = RIG_UNSELECTED; + currentFrame = RIG_UNSELECTED; + playCursor = 0.0f; + playing = false; +} + +/*****************************************************************************/ +void Rig::update() +{ + Animation * a = currentAnimationPtr(); + if (!a || a->frames.isEmpty()) return; + if (!playing) return; + + playCursor += 1.0f; + if (playCursor >= a->frames.count()) + playCursor -= a->frames.count(); + + currentFrame = (int)floorf(playCursor); +} + +/*****************************************************************************/ +int Rig::animationAdd(const char * name) +{ + Animation a; + memset(&a.name, 0, sizeof(a.name)); + if (name) strncpy(a.name, name, RIG_NAME_MAX); + a.frames.append(Frame()); + + animations.append(a); + currentAnimation = animations.count() - 1; + currentFrame = 0; + playCursor = 0.0f; + return currentAnimation; +} + +void Rig::animationDelete(int aId) +{ + if (aId < 0 || aId >= animations.count()) return; + animations.removeAt(aId); + + if (animations.isEmpty()) { + currentAnimation = RIG_UNSELECTED; + currentFrame = RIG_UNSELECTED; + } else if (currentAnimation >= animations.count()) { + animationSelect(animations.count() - 1); + } +} + +void Rig::animationSelect(int aId) +{ + if (aId < 0 || aId >= animations.count()) return; + currentAnimation = aId; + currentFrame = animations[aId].frames.isEmpty() ? RIG_UNSELECTED : 0; + playCursor = 0.0f; +} + +/*****************************************************************************/ +int Rig::frameAdd() +{ + Animation * a = currentAnimationPtr(); + if (!a) return RIG_UNSELECTED; + + Frame f; + if (currentFrame >= 0 && currentFrame < a->frames.count()) + f = a->frames[currentFrame]; // duplicate the current pose + a->frames.append(f); + + currentFrame = a->frames.count() - 1; + return currentFrame; +} + +int Rig::frameInsert(int at) +{ + Animation * a = currentAnimationPtr(); + if (!a) return RIG_UNSELECTED; + if (at < 0 || at > a->frames.count()) return RIG_UNSELECTED; + + Frame f; + if (currentFrame >= 0 && currentFrame < a->frames.count()) + f = a->frames[currentFrame]; // duplicate the current pose + a->frames.insert(at, f); + + currentFrame = at; + return currentFrame; +} + +void Rig::frameDelete(int fId) +{ + Animation * a = currentAnimationPtr(); + if (!a) return; + if (fId < 0 || fId >= a->frames.count()) return; + + a->frames.removeAt(fId); + + if (a->frames.isEmpty()) { + currentFrame = RIG_UNSELECTED; + } else if (currentFrame >= a->frames.count()) { + currentFrame = a->frames.count() - 1; + } + playCursor = (float)(currentFrame < 0 ? 0 : currentFrame); +} + +void Rig::frameSelect(int fId) +{ + Animation * a = currentAnimationPtr(); + if (!a) return; + if (fId < 0 || fId >= a->frames.count()) return; + + currentFrame = fId; + playCursor = (float)fId; +} + +/*****************************************************************************/ +void Rig::play() +{ + Animation * a = currentAnimationPtr(); + if (!a || a->frames.isEmpty()) return; + playing = true; +} + +void Rig::stop() +{ + playing = false; +} + +/*****************************************************************************/ +Animation * Rig::currentAnimationPtr() +{ + if (currentAnimation < 0 || currentAnimation >= animations.count()) + return nullptr; + return &animations[currentAnimation]; +} + +Frame * Rig::currentFramePtr() +{ + Animation * a = currentAnimationPtr(); + if (!a) return nullptr; + if (currentFrame < 0 || currentFrame >= a->frames.count()) + return nullptr; + return &a->frames[currentFrame]; +} diff --git a/common/engine/rig.h b/common/engine/rig.h new file mode 100644 index 0000000..acfee58 --- /dev/null +++ b/common/engine/rig.h @@ -0,0 +1,93 @@ +/** + DIE ENGINE + Depth Integration Engine / A modern ray-caster + (c) Fred's Lab 2024-2026 + Frédéric Meslin / info@fredslab.net + SPDX-License-Identifier: MIT + If used commercially, contributions, donations are highly appreciated. + + rig model +*/ + +#ifndef RIG_H +#define RIG_H + +#include "rigobjects.h" + +#include +#include +#include + +#include + +static constexpr int RIG_NAME_MAX = 31; + +/*****************************************************************************/ +/** + \brief A single pose: the joint positions for one frame + + Bones are not stored here; the skeleton topology lives on the Rig and is + shared by every frame (bones reference joints by index). +*/ +struct Frame { + QList joints; +}; + +/** + \brief A named, ordered set of frames +*/ +struct Animation { + char name[RIG_NAME_MAX + 1]; + QList frames; +}; + +/*****************************************************************************/ +class Rig +{ +public: + Rig(); + + void init(); + void terminate(); + + /// \brief Advance the play cursor while playing, then refresh currentFrame + void update(); + + bool save(const QString & filename); + bool load(const QString & filename); + +// Animations + int animationAdd(const char * name); + void animationDelete(int aId); + void animationSelect(int aId); + +// Frames, within the current animation + int frameAdd(); ///< append a copy of the current frame + int frameInsert(int at); ///< insert a copy of the current frame at index + void frameDelete(int fId); + void frameSelect(int fId); + +// Playback + void play(); + void stop(); + +// Convenience access to the current animation / frame, nullptr when none + Animation * currentAnimationPtr(); + Frame * currentFramePtr(); + + QString path; + QImage textures; + + QList bones; ///< skeleton topology, shared by every frame + QList animations; + + int currentAnimation; + int currentFrame; + float playCursor; + bool playing; + +private: + void clearAnimations(); +}; + +#endif // RIG_H diff --git a/common/engine/rig_io.cpp b/common/engine/rig_io.cpp new file mode 100644 index 0000000..4f1a8e0 --- /dev/null +++ b/common/engine/rig_io.cpp @@ -0,0 +1,183 @@ +/** + DIE ENGINE + Depth Integration Engine / A modern ray-caster + (c) Fred's Lab 2024-2026 + Frédéric Meslin / info@fredslab.net + SPDX-License-Identifier: MIT + If used commercially, contributions, donations are highly appreciated. + + rig load/save +*/ + +#include "rig.h" + +#include +#include + +#include +#include +#include + +// Maximum size of an embedded texture strip (base64 data) +static constexpr uint64_t TEXTURE_DATA_MAX = 16ull << 20; + +/*****************************************************************************/ +static void skipLine(FILE * file) +{ + while (true) { + int c = fgetc(file); + if (c == EOF) return; + if (c == '\n') return; + } +} + +/*****************************************************************************/ +bool Rig::save(const QString & filename) +{ + FILE * file = fopen(filename.toLocal8Bit().constData(), "wb"); + if (!file) return false; + + fprintf(file, "# == DIE RIG == \n"); + fprintf(file, "# Fred's Lab 2024-2026\n"); + fprintf(file, "# 26.06.26 - V1.0\n"); + +// ==== BONES ==== + fprintf(file, "# == BONES ==\n"); + fprintf(file, "# format: B jointID1, jointID2, width, length, offset, imageCount, flags\n"); + fprintf(file, "# format: I index, imageID\n"); + for (const Bone & b : bones) { + fprintf(file, "B %04hu, %04hu, %+4.4f, %+4.4f, %+4.4f, %02hu, %04hx\n", + b.jointID1, b.jointID2, b.width, b.length, b.offset, b.imageCount, b.flags); + uint16_t count = b.imageCount > RIG_BONE_IMAGES_MAX ? RIG_BONE_IMAGES_MAX : b.imageCount; + for (int i = 0; i < count; i++) + fprintf(file, "\tI %02d, %04hu\n", i, b.images[i]); + } + fprintf(file, "\n"); + +// ==== ANIMATIONS ==== + fprintf(file, "# == ANIMATIONS ==\n"); + fprintf(file, "# format: A name\n"); + fprintf(file, "# format: F index\n"); + fprintf(file, "# format: J x, y, z, flags\n"); + for (const Animation & a : animations) { + const char * name = a.name[0] ? a.name : "None"; + fprintf(file, "A %.31s\n", name); + for (int f = 0; f < a.frames.count(); f++) { + fprintf(file, "\tF %04d\n", f); + for (const Joint & j : a.frames[f].joints) + fprintf(file, "\t\tJ %+4.4f, %+4.4f, %+4.4f, %04hx\n", + j.pos.x(), j.pos.y(), j.pos.z(), j.flags); + } + } + fprintf(file, "\n"); + +// ==== TEXTURE STRIP ==== + fprintf(file, "# == TEXTURE STRIP ==\n"); + fprintf(file, "# format: T type, length, data\n"); + + QByteArray imageData; + QBuffer buffer(&imageData); + buffer.open(QIODevice::WriteOnly); + textures.save(&buffer, "PNG"); + QByteArray base64Data = imageData.toBase64(); + + fprintf(file, "T %02d, %llx\n", 0, (unsigned long long) base64Data.size()); + fwrite(base64Data.constData(), base64Data.size(), 1, file); + fprintf(file, "\n\n"); + + fclose(file); + return true; +} + +bool Rig::load(const QString & filename) +{ + FILE * file = fopen(filename.toLocal8Bit().constData(), "rb"); + if (!file) { + qWarning() << "RIG Resource unavailable: " << filename << "\n"; + return false; + } + + path = filename; + + animations.clear(); + bones.clear(); + currentAnimation = RIG_UNSELECTED; + currentFrame = RIG_UNSELECTED; + playCursor = 0.0f; + playing = false; + + while (true) { + int c = fgetc(file); + if (c == EOF) { + break; + + }else if (c == '#') { + skipLine(file); + continue; + + }else if (c == '\t' || c == '\n' || c == '\r' || c == ' ') { + continue; + + }else if (c == 'B') { + Bone b{}; + fscanf(file, "%hu, %hu, %f, %f, %f, %hu, %hx\n", + &b.jointID1, &b.jointID2, &b.width, &b.length, &b.offset, &b.imageCount, &b.flags); + if (b.imageCount > RIG_BONE_IMAGES_MAX) b.imageCount = RIG_BONE_IMAGES_MAX; + bones.append(b); + + }else if (c == 'I') { + uint16_t index = 0, imageID = 0; + fscanf(file, "%hu, %hu\n", &index, &imageID); + if (!bones.isEmpty() && index < RIG_BONE_IMAGES_MAX) + bones.last().images[index] = imageID; + + }else if (c == 'A') { + Animation a; + a.name[0] = 0; + fscanf(file, "%31s\n", a.name); + a.name[RIG_NAME_MAX] = 0; + animations.append(a); + + }else if (c == 'F') { + int index = 0; + fscanf(file, "%d\n", &index); + if (!animations.isEmpty()) + animations.last().frames.append(Frame()); + + }else if (c == 'J') { + Joint j{}; + float x, y, z; + fscanf(file, "%f, %f, %f, %hx\n", &x, &y, &z, &j.flags); + j.pos = QVector3D(x, y, z); + j.apos = j.pos; + if (!animations.isEmpty() && !animations.last().frames.isEmpty()) + animations.last().frames.last().joints.append(j); + + }else if (c == 'T') { + uint16_t type = 0; + uint64_t size = 0; + fscanf(file, "%hu, %llx\n", &type, &size); + if (type > 0 || size == 0 || size > TEXTURE_DATA_MAX) { + skipLine(file); + continue; + } + + QByteArray base64Data((qsizetype) size, 0); + if (fread(base64Data.data(), size, 1, file) != 1) { + skipLine(file); + continue; + } + skipLine(file); + + QByteArray imageData = QByteArray::fromBase64(base64Data); + textures.loadFromData(imageData, "PNG"); + } + } + + fclose(file); + + if (!animations.isEmpty()) + animationSelect(0); + + return true; +} diff --git a/common/engine/rigobjects.h b/common/engine/rigobjects.h new file mode 100644 index 0000000..acd515b --- /dev/null +++ b/common/engine/rigobjects.h @@ -0,0 +1,63 @@ +/** + DIE ENGINE + Depth Integration Engine / A modern ray-caster + (c) Fred's Lab 2024-2026 + Frédéric Meslin / info@fredslab.net + SPDX-License-Identifier: MIT + If used commercially, contributions, donations are highly appreciated. + + rig objects +*/ + +#ifndef RIGOBJECTS_H +#define RIGOBJECTS_H + +#include +#include + +static constexpr int RIG_BONE_IMAGES_MAX = 8; +static constexpr int RIG_UNSELECTED = -1; + +/*****************************************************************************/ +typedef enum : uint16_t { + JOINT_FLAG_FREE = 0x0000, + JOINT_FLAG_USED = 0x0001, +} JOINT_FLAGS; + +/** + \brief Rig joint: a 3D articulation point shared by the bones +*/ +typedef struct { + QVector3D pos; ///< rest position, as authored in the editor + QVector3D apos; ///< animation state, interpolated every frame + + uint16_t flags; + bool selected; +} Joint; + +/*****************************************************************************/ +typedef enum : uint16_t { + BONE_FLAG_FREE = 0x0000, + BONE_FLAG_INVISIBLE = 0x0001, + BONE_FLAG_MIRROR = 0x0002, ///< flip the image horizontally (left / right reuse) +} BONE_FLAGS; + +/** + \brief Rig bone: a camera-facing quad spanning two joints, one image per view arc +*/ +typedef struct { + uint16_t jointID1; ///< base joint + uint16_t jointID2; ///< tip joint + + float width; ///< quad width across the bone, in world units + float length; ///< extra quad length added to the joint span, in world units + float offset; ///< quad shift along the bone axis, in world units + + uint16_t images[RIG_BONE_IMAGES_MAX]; ///< per-arc image ids, [0] centred on the front + uint16_t imageCount; ///< number of arcs (1, 2, 4, 8...) + + uint16_t flags; + bool selected; +} Bone; + +#endif // RIGOBJECTS_H From c72e2d4e63b0f81d8c3f9f337a9e3112286744cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Meslin?= Date: Sat, 27 Jun 2026 19:54:53 +0200 Subject: [PATCH 2/3] Consolidate shared rendering primitives into common/engine Move drawer.cpp/h from animator/ to common/engine/ so Waller and Rigger can share the same software quad/triangle rasteriser. Extract Viewpoint, Texture, D2R/R2D into a new primitives.h, and scope the renderer's flags as the nested Renderer::FLAGS enum instead of free RENDERER_FLAG_* macros. Add colorsAlphaBlendSSE4 (used by the drawer) to colors.h. Updates Waller's call sites and build file for the renamed flags and the new primitives.h header. --- common/engine/colors.h | 15 ++ common/engine/drawer.cpp | 224 ++++++++++++++++++++++++++++++ common/engine/drawer.h | 25 ++++ common/engine/primitives.h | 59 ++++++++ common/engine/renderer.cpp | 47 ++++--- common/engine/renderer.h | 67 +++------ common/engine/renderer_config.cpp | 34 ++--- editor/mainwindow.cpp | 64 ++++----- editor/mainwindow.ui | 36 +++-- editor/waller.pro | 1 + editor/wdgmapeditor.cpp | 2 +- 11 files changed, 436 insertions(+), 138 deletions(-) create mode 100644 common/engine/drawer.cpp create mode 100644 common/engine/drawer.h create mode 100644 common/engine/primitives.h diff --git a/common/engine/colors.h b/common/engine/colors.h index ce5c219..0e2a580 100644 --- a/common/engine/colors.h +++ b/common/engine/colors.h @@ -121,6 +121,21 @@ inline uint32_t colorsScaleAccumulateSSE4(uint32_t color1, uint32_t color2, uint return _mm_cvtsi128_si32(result); } +/** + \brief Alpha-blend a packed ARGB source over a destination color + \param dst packed 8-bit ARGB background color + \param src packed 8-bit ARGB foreground color (its A component drives the blend) + \return dst * (1 - srcA) + src * srcA, packed ARGB color +*/ +inline uint32_t colorsAlphaBlendSSE4(uint32_t dst, uint32_t src) +{ +// Promote the source alpha from 0..255 to a 0..256 Q8.8 factor so that a +// fully opaque source (255) maps to 1.0 and leaves no background bleed + uint16_t a = (uint16_t) (src >> 24); + a += a >> 7; + return colorsLinearSSE4(dst, src, a); +} + /*****************************************************************************/ /** \brief Linearly interpolate between two color vectors diff --git a/common/engine/drawer.cpp b/common/engine/drawer.cpp new file mode 100644 index 0000000..797bf67 --- /dev/null +++ b/common/engine/drawer.cpp @@ -0,0 +1,224 @@ +/** + DIE ENGINE + Depth Integration Engine / A modern ray-caster + (c) Fred's Lab 2024-2026 + Frédéric Meslin / info@fredslab.net + SPDX-License-Identifier: MIT + If used commercially, contributions, donations are highly appreciated. + + 2d engine +*/ + +#include "drawer.h" +#include "primitives.h" +#include "colors.h" + +#include + +#include +#include +#include + +/*****************************************************************************/ +static void drawTriangle(QImage & image, const Triangle * triangle, const Texture & texture); + +/*****************************************************************************/ +void drawerQuad(QImage & image, const Quad * quad, const Texture & texture) +{ + Triangle tris[2] = { + { + {quad->xs[0], quad->xs[1], quad->xs[2]}, + {quad->ys[0], quad->ys[1], quad->ys[2]}, + {quad->us[0], quad->us[1], quad->us[2]}, + {quad->vs[0], quad->vs[1], quad->vs[2]}, + quad->surfaceId, + },{ + {quad->xs[0], quad->xs[2], quad->xs[3]}, + {quad->ys[0], quad->ys[2], quad->ys[3]}, + {quad->us[0], quad->us[2], quad->us[3]}, + {quad->vs[0], quad->vs[2], quad->vs[3]}, + quad->surfaceId, + } + }; + + drawerTriangle(image, &tris[0], texture); + drawerTriangle(image, &tris[1], texture); +} + +/*****************************************************************************/ +void drawerTriangle(QImage & image, const Triangle * triangle, const Texture & texture) +{ + int low, mid, high; + +// Sort the 3 vertices by ascending y: low <= mid <= high + if (triangle->ys[0] < triangle->ys[1]) {low = 0; high = 1;} + else {low = 1; high = 0;} + if (triangle->ys[2] < triangle->ys[low]) {mid = low; low = 2;} + else {mid = 2;} + + if (triangle->ys[mid] > triangle->ys[high]) { + int tmp = mid; mid = high; high = tmp; + } + +// Reject degenerate (zero-height) triangles + float dy = triangle->ys[high] - triangle->ys[low]; + if ((int) dy <= 0) return; + +// Split into a flat-bottom and a flat-top triangle, joined at the mid +// scanline. When the triangle already has a flat top or bottom edge a +// single triangle is enough. Vertex 0 of every emitted triangle is its +// apex (the lone vertex), so drawTriangle never has to sort. + Triangle tris[2]; + int triCount = 1; + + int topH = (int) triangle->ys[mid] - (int) triangle->ys[low]; + int botH = (int) triangle->ys[high] - (int) triangle->ys[mid]; + + if (topH == 0) { + // Already flat-topped: apex is the high vertex + tris[0] = { + {triangle->xs[high], triangle->xs[low], triangle->xs[mid]}, + {triangle->ys[high], triangle->ys[low], triangle->ys[mid]}, + {triangle->us[high], triangle->us[low], triangle->us[mid]}, + {triangle->vs[high], triangle->vs[low], triangle->vs[mid]}, + triangle->surfaceId, + }; + + } else if (botH == 0) { + // Already flat-bottomed: apex is the low vertex + tris[0] = { + {triangle->xs[low], triangle->xs[mid], triangle->xs[high]}, + {triangle->ys[low], triangle->ys[mid], triangle->ys[high]}, + {triangle->us[low], triangle->us[mid], triangle->us[high]}, + {triangle->vs[low], triangle->vs[mid], triangle->vs[high]}, + triangle->surfaceId, + }; + + } else { + // Interpolate the split vertex 'e' on the long low -> high edge, + // at the mid scanline (k is the position of mid along that edge). + float k = (triangle->ys[mid] - triangle->ys[low]) / dy; + float xe = triangle->xs[low] + (triangle->xs[high] - triangle->xs[low]) * k; + float ye = triangle->ys[low] + (triangle->ys[high] - triangle->ys[low]) * k; + float ue = triangle->us[low] + (triangle->us[high] - triangle->us[low]) * k; + float ve = triangle->vs[low] + (triangle->vs[high] - triangle->vs[low]) * k; + + // Upper flat-bottom triangle, apex = low, base = (mid, e) + tris[0] = { + {triangle->xs[low], triangle->xs[mid], xe}, + {triangle->ys[low], triangle->ys[mid], ye}, + {triangle->us[low], triangle->us[mid], ue}, + {triangle->vs[low], triangle->vs[mid], ve}, + triangle->surfaceId, + }; + // Lower flat-top triangle, apex = high, base = (mid, e) + tris[1] = { + {triangle->xs[high], triangle->xs[mid], xe}, + {triangle->ys[high], triangle->ys[mid], ye}, + {triangle->us[high], triangle->us[mid], ue}, + {triangle->vs[high], triangle->vs[mid], ve}, + triangle->surfaceId, + }; + triCount++; + } + + drawTriangle(image, &tris[0], texture); + if (triCount == 1) return; + drawTriangle(image, &tris[1], texture); +} + +/*****************************************************************************/ +void drawTriangle(QImage & image, const Triangle * triangle, const Texture & texture) +{ + uint32_t * frame = (uint32_t *) image.bits(); + uint32_t * tex = texture.pixels + texture.block * triangle->surfaceId; + + const int width = image.width(); + const int height = image.height(); + const int stride = image.bytesPerLine() / 4; + const int mask = texture.mask; + const int size = texture.size; + +// Vertex 0 is the apex, vertices 1 and 2 form the horizontal base + float ax = triangle->xs[0], ay = triangle->ys[0]; + + float sz = (float) size; + float au = triangle->us[0] * sz, av = triangle->vs[0] * sz; + + float fullDy = triangle->ys[1] - ay; + if (fullDy == 0.0f) return; + float invDy = 1.0f / fullDy; + +// Per-scanline increments along both apex -> base edges (DDA): computed +// once, then accumulated each row instead of re-interpolating from scratch + float xaInc = (triangle->xs[1] - ax) * invDy; + float uaInc = (triangle->us[1] * sz - au) * invDy; + float vaInc = (triangle->vs[1] * sz - av) * invDy; + float xbInc = (triangle->xs[2] - ax) * invDy; + float ubInc = (triangle->us[2] * sz - au) * invDy; + float vbInc = (triangle->vs[2] * sz - av) * invDy; + +// Walk every scanline covered by the triangle (apex and base, top first) + float ybase = triangle->ys[1]; + int yStart = (int) ceilf(ay < ybase ? ay : ybase); + int yStop = (int) ceilf(ay < ybase ? ybase : ay); + if (yStart < 0) yStart = 0; + if (yStop > height) yStop = height; + +// Seed the edge accumulators at the first (top-clipped) scanline + float prestep = (float) yStart - ay; + float xa = ax + xaInc * prestep, ua = au + uaInc * prestep, va = av + vaInc * prestep; + float xb = ax + xbInc * prestep, ub = au + ubInc * prestep, vb = av + vbInc * prestep; + + for (int y = yStart; y < yStop; y++) { + // Span runs from edge a to edge b; the drawing direction does not matter + // so the u/v slope is anchored at edge a and the bounds just use min/max + float dx = xb - xa; + if (dx != 0.0f) { + float invDx = 1.0f / dx; + float uInc = (ub - ua) * invDx; + float vInc = (vb - va) * invDx; + + int xStart = (int) ceilf(xa < xb ? xa : xb); + int xStop = (int) ceilf(xa < xb ? xb : xa); + if (xStart < 0) xStart = 0; + if (xStop > width) xStop = width; + + // Seed u/v on the span line at the first pixel (anchored at edge a) + float preX = (float) xStart - xa; + float uFloat = ua + uInc * preX; + float vFloat = va + vInc * preX; + + uint32_t * row = frame + y * stride; + + for (int x = xStart; x < xStop; x++) { + // Bilinear texture fetch, wrapping on the power-of-two texture mask + int u = (int) uFloat; + int v = (int) vFloat; + uint16_t uFrac = (uint16_t) ((uFloat - u) * 256.0f); + uint16_t vFrac = (uint16_t) ((vFloat - v) * 256.0f); + + uint32_t src00 = tex[(u & mask) * size + (v & mask)]; + uint32_t src01 = tex[((u + 1) & mask) * size + (v & mask)]; + uint32_t src10 = tex[(u & mask) * size + ((v + 1) & mask)]; + uint32_t src11 = tex[((u + 1) & mask) * size + ((v + 1) & mask)]; + + uint32_t src0 = colorsLinearSSE4(src00, src01, uFrac); + uint32_t src1 = colorsLinearSSE4(src10, src11, uFrac); + uint32_t src = colorsLinearSSE4(src0, src1, vFrac); + + // Blend over the frame, skipping fully transparent texels + if (src >= 0x01000000) row[x] = colorsAlphaBlendSSE4(row[x], src); + + uFloat += uInc; + vFloat += vInc; + } + } + + // Step both edges down to the next scanline + xa += xaInc; ua += uaInc; va += vaInc; + xb += xbInc; ub += ubInc; vb += vbInc; + } +} + + diff --git a/common/engine/drawer.h b/common/engine/drawer.h new file mode 100644 index 0000000..015e51f --- /dev/null +++ b/common/engine/drawer.h @@ -0,0 +1,25 @@ +/** + RIGGER + Animation editor for the DIE engine + (c) Fred's Lab 2024-2026 + Frédéric Meslin / info@fredslab.net + SPDX-License-Identifier: MIT + If used commercially, contributions, donations are highly appreciated. + + rigger model +*/ + +#ifndef DRAWER_H +#define DRAWER_H + + #include "primitives.h" + + #include + + #include + + // Vertex UVs (us / vs) are normalised: 0.0 .. 1.0 spans one texture tile. + void drawerQuad(QImage & image, const Quad * quad, const Texture & texture); + void drawerTriangle(QImage & image, const Triangle * triangle, const Texture & texture); + +#endif //DRAWER_H \ No newline at end of file diff --git a/common/engine/primitives.h b/common/engine/primitives.h new file mode 100644 index 0000000..d83b370 --- /dev/null +++ b/common/engine/primitives.h @@ -0,0 +1,59 @@ +/** + RIGGER + Animation editor for the DIE engine + (c) Fred's Lab 2024-2026 + Frédéric Meslin / info@fredslab.net + SPDX-License-Identifier: MIT + If used commercially, contributions, donations are highly appreciated. + + graphic promitives +*/ + +#ifndef PRIMITIVES_H +#define PRIMITIVES_H + + #include + #include + #include + + constexpr float D2R = 3.14159265f / 180.0f; + constexpr float R2D = 180.0f / 3.14159265f; + + typedef struct { + QVector3D pos; + QVector3D offset; + float pan; + float tilt; + } Viewpoint; + + typedef struct { + float pan; ///< rotation about the Y axis, in degrees + float diameter; ///< cylinder diameter / camera Z distance + float y; ///< vertical offset + } ViewpointOrtho; + + typedef struct { + uint32_t * pixels; + uint16_t size; + uint16_t count; + uint16_t mask; + uint32_t block; + } Texture; + + typedef struct { + float xs[4]; + float ys[4]; + float us[4]; + float vs[4]; + uint16_t surfaceId; + }Quad; + + typedef struct { + float xs[3]; + float ys[3]; + float us[3]; + float vs[3]; + uint16_t surfaceId; + }Triangle; + +#endif diff --git a/common/engine/renderer.cpp b/common/engine/renderer.cpp index 332682e..98bf9da 100644 --- a/common/engine/renderer.cpp +++ b/common/engine/renderer.cpp @@ -12,6 +12,7 @@ #include "renderer.h" #include "colors.h" #include "postfx.h" +#include "primitives.h" #include "workerpool.h" #include @@ -31,7 +32,7 @@ constexpr float HeightEpsilon = 0x1p-5f; /*****************************************************************************/ Renderer::Renderer() : - flags(RENDERER_FLAGS_DEFAULT), + flags(FLAGS_DEFAULT), nodes(nullptr), nodesCount(0), nodesAllocated(0), walls(nullptr), wallsCount(0), wallsAllocated(0), textures(nullptr), texturesCount(0), texturesAllocated(0), @@ -75,12 +76,12 @@ void Renderer::terminate() } /*****************************************************************************/ -void Renderer::setFlags(RENDERER_FLAGS flags) +void Renderer::setFlags(FLAGS flags) { this->flags = flags; } -void Renderer::checkFlag(RENDERER_FLAGS flag, bool checked) +void Renderer::checkFlag(FLAGS flag, bool checked) { flags &= ~(uint32_t) flag; if (checked) flags |= flag; @@ -347,17 +348,17 @@ void Renderer::lightsMash() // Compute the glowmaps glowBleed = _mm_set1_ps(GlowBleedFactor); const size_t gmBytes = glowmapSize * glowmapSize * sizeof(__m128); - if (!(flags & RENDERER_FLAG_LIGHTS)) { + if (!(flags & Renderer::FLAG_LIGHTS)) { memset(glowmap, 0, gmBytes); glowmapDirtyLast = {{0, glowmapSize, 0, glowmapSize}}; return; } - if (flags & RENDERER_FLAG_GLOWMAP_REBUILD) { + if (flags & Renderer::FLAG_GLOWMAP_REBUILD) { memset(glowmapStill, 0, gmBytes); - if (flags & RENDERER_FLAG_MULTITHREADING) glowmapConcurrent(glowmapStill, true); + if (flags & Renderer::FLAG_MULTITHREADING) glowmapConcurrent(glowmapStill, true); else glowmapChunk(glowmapStill, true, 0, glowmapSize); - flags &= ~RENDERER_FLAG_GLOWMAP_REBUILD; + flags &= ~Renderer::FLAG_GLOWMAP_REBUILD; // The still glowmap just changed everywhere, so resync the whole dynamic glowmap this frame dirtyBoxes.append({0, glowmapSize, 0, glowmapSize}); } @@ -366,7 +367,7 @@ void Renderer::lightsMash() glowmapDirtyBoxes = glowmapDirtyLast + dirtyBoxes; glowmapDirtyLast = std::move(dirtyBoxes); - if (flags & RENDERER_FLAG_MULTITHREADING) glowmapConcurrent(glowmap, false); + if (flags & Renderer::FLAG_MULTITHREADING) glowmapConcurrent(glowmap, false); else glowmapChunk(glowmap, false, 0, glowmapSize); } @@ -374,15 +375,15 @@ void Renderer::lightsMash() inline void Renderer::sceneBoundsInit() { sceneCornerBegin = QVector2D(+32768.0f, +32768.0f); - sceneCornerEnd = QVector2D(-32768.0f, -32768.0f); + sceneCornerEnd = QVector2D(-32768.0f, -32768.0f); } inline void Renderer::sceneBoundsRegister(float x, float z) { if (x < sceneCornerBegin.x()) sceneCornerBegin.setX(x); - if (x > sceneCornerEnd.x()) sceneCornerEnd.setX(x); + if (x > sceneCornerEnd.x()) sceneCornerEnd.setX(x); if (z < sceneCornerBegin.y()) sceneCornerBegin.setY(z); - if (z > sceneCornerEnd.y()) sceneCornerEnd.setY(z); + if (z > sceneCornerEnd.y()) sceneCornerEnd.setY(z); } /*****************************************************************************/ @@ -476,23 +477,23 @@ void Renderer::render(Viewpoint & vp) // Render frame viewPoint = vp; - if (flags & RENDERER_FLAG_MULTITHREADING) renderConcurrent(); + if (flags & Renderer::FLAG_MULTITHREADING) renderConcurrent(); else renderChunk(renderStates[0], 0, frameResoX); // Render post-fx - if (flags & RENDERER_FLAG_MOTIONBLUR) { + if (flags & Renderer::FLAG_MOTIONBLUR) { uint16_t blend = motionBlurFactor * 256.0f * 0.01f; motionBlurSSE4(frame, frameLast, blend, frameResoX * frameResoY); } - if (flags & RENDERER_FLAG_VIGNETTE) { + if (flags & Renderer::FLAG_VIGNETTE) { uint16_t inner = vignetteInnerRadius * frameResoY * 0.005f; uint16_t outer = vignetteOuterRadius * frameResoY * 0.005f; if (outer <= inner) outer = inner + 1; vignetteSSE4(frame, frameResoX, frameResoY, inner, outer); } - if (flags & RENDERER_FLAG_GAMMA) { + if (flags & Renderer::FLAG_GAMMA) { float ks[3] = {gammaKRed, gammaKGreen, gammaKBlue}; gammaSSE4(frame, frameResoX, frameResoY, ks); } @@ -652,7 +653,7 @@ void Renderer::renderChunk(Context & state, int x1, int x2) renderVertical(state, 0, stackBelow, stackAbove, 0, frameResoY); // Render walls - if (flags & RENDERER_FLAG_WALLS) drawVStrips(state); + if (flags & Renderer::FLAG_WALLS) drawVStrips(state); } } @@ -728,7 +729,7 @@ void Renderer::renderVertical(Context & state, int depth, if (bot < 0) bot = 0; if (wBot < mustBelow && wBot > mustAbove) { - if (flags & RENDERER_FLAG_SURFACES) + if (flags & Renderer::FLAG_SURFACES) surfaceDraw(state, w, WALL_SURFACE_FLOOR, scanTop, bot, wBot); if (stackBelow.length() > 1) stackBelow.removeLast(); renderVertical(state, i + 1, stackBelow, stackAbove, bot, scanBot); @@ -743,7 +744,7 @@ void Renderer::renderVertical(Context & state, int depth, if (top > frameResoY) top = frameResoY; if (wTop > mustAbove && wTop < mustBelow) { - if (flags & RENDERER_FLAG_SURFACES) + if (flags & Renderer::FLAG_SURFACES) surfaceDraw(state, w, WALL_SURFACE_CEILING, top, scanBot, wTop); if (stackAbove.length() > 1) stackAbove.removeLast(); renderVertical(state, i + 1, stackBelow, stackAbove, scanTop, top); @@ -762,7 +763,7 @@ void Renderer::renderVertical(Context & state, int depth, if (strip.yTop > scanTop) { int bot = std::min((int)strip.yTop, scanBot); if ((wTop > mustAbove) && (strip.flags & VSTRIP_FLAG_HASCEILING)) { - if (flags & RENDERER_FLAG_SURFACES) + if (flags & Renderer::FLAG_SURFACES) surfaceDraw(state, w, WALL_SURFACE_CEILING, scanTop, bot, wTop); } else { float ma = mustAbove; @@ -784,7 +785,7 @@ void Renderer::renderVertical(Context & state, int depth, if (strip.yBot < scanBot) { int top = std::max((int)strip.yBot, scanTop); if ((wBot < mustBelow) && (strip.flags & VSTRIP_FLAG_HASFLOOR)) { - if (flags & RENDERER_FLAG_SURFACES) + if (flags & Renderer::FLAG_SURFACES) surfaceDraw(state, w, WALL_SURFACE_FLOOR, top, scanBot, wBot); } else { float mb = mustBelow; @@ -887,7 +888,7 @@ void Renderer::surfaceDraw(Context & state, const Wall & w, uint16_t sID, int sc float aoScale = occlusionLength > 0.0f ? occlusionDarken / occlusionLength : 0.0f; int yAoEnd; - if (!(flags & RENDERER_FLAG_AMBIENT_OCCLUSION) || (w.flags & WALL_FLAG_ALPHA)) + if (!(flags & Renderer::FLAG_AMBIENT_OCCLUSION) || (w.flags & WALL_FLAG_ALPHA)) yAoEnd = yStart; else if (adjacentAoEnd <= 0.0f) yAoEnd = yEnd; else { @@ -1096,8 +1097,8 @@ void Renderer::vstripDraw(Context & state, const Strip & strip) }; // Occlusion boundaries, clamped to rendered range - int topAoEnd = (flags & RENDERER_FLAG_AMBIENT_OCCLUSION) ? std::clamp((int)strip.yDarkenTop, scanTop, scanBot) : scanTop; - int botAoStart = (flags & RENDERER_FLAG_AMBIENT_OCCLUSION) ? std::clamp((int)strip.yDarkenBot, scanTop, scanBot) : scanBot; + int topAoEnd = (flags & Renderer::FLAG_AMBIENT_OCCLUSION) ? std::clamp((int)strip.yDarkenTop, scanTop, scanBot) : scanTop; + int botAoStart = (flags & Renderer::FLAG_AMBIENT_OCCLUSION) ? std::clamp((int)strip.yDarkenBot, scanTop, scanBot) : scanBot; topAoEnd = std::min(topAoEnd, botAoStart); // Top gradient: (1-occlusionDarken) at yNoclipTop -> 1.0 at yDarkenTop diff --git a/common/engine/renderer.h b/common/engine/renderer.h index 42eba97..696391d 100644 --- a/common/engine/renderer.h +++ b/common/engine/renderer.h @@ -14,9 +14,8 @@ #include "mapobjects.h" #include "workerpool.h" +#include "primitives.h" -#include -#include #include #include @@ -25,36 +24,6 @@ #include -/*****************************************************************************/ -typedef enum : uint32_t { - RENDERER_FLAG_WALLS = 0x0001, - RENDERER_FLAG_SURFACES = 0x0002, - RENDERER_FLAG_LIGHTS = 0x0004, - RENDERER_FLAG_AMBIENT_OCCLUSION = 0x0008, - RENDERER_FLAG_GLOWMAP_REBUILD = 0x0010, - RENDERER_FLAG_MOTIONBLUR = 0x0100, - RENDERER_FLAG_VIGNETTE = 0x0200, - RENDERER_FLAG_GAMMA = 0x0400, - RENDERER_FLAG_MULTITHREADING = 0x1000, - RENDERER_FLAG_ALPHA_FEATURES = 0x2000, - RENDERER_FLAGS_DEFAULT = RENDERER_FLAG_WALLS | RENDERER_FLAG_SURFACES | RENDERER_FLAG_LIGHTS | RENDERER_FLAG_AMBIENT_OCCLUSION | RENDERER_FLAG_GAMMA, -} RENDERER_FLAGS; - -/*****************************************************************************/ -/** - \brief Camera position and orientation -*/ -typedef struct { - QVector3D pos; - QVector3D offset; - float pan; - float tilt; -} Viewpoint; - -/*****************************************************************************/ -constexpr float D2R = 3.14159265f / 180.0f; -constexpr float R2D = 180.0f / 3.14159265f; - /*****************************************************************************/ class Renderer { @@ -119,6 +88,21 @@ class Renderer /// \brief Sentinel returned by clickGetWallID when no wall was picked static constexpr uint16_t ClickNoWall = 0xFFFF; + /// \brief Renderer enable/disable flags + typedef enum : uint32_t { + FLAG_WALLS = 0x0001, + FLAG_SURFACES = 0x0002, + FLAG_LIGHTS = 0x0004, + FLAG_AMBIENT_OCCLUSION = 0x0008, + FLAG_GLOWMAP_REBUILD = 0x0010, + FLAG_MOTIONBLUR = 0x0100, + FLAG_VIGNETTE = 0x0200, + FLAG_GAMMA = 0x0400, + FLAG_MULTITHREADING = 0x1000, + FLAG_ALPHA_FEATURES = 0x2000, + FLAGS_DEFAULT = FLAG_WALLS | FLAG_SURFACES | FLAG_LIGHTS | FLAG_AMBIENT_OCCLUSION | FLAG_GAMMA, + } FLAGS; + /// \brief Renderer-side node (compact variant) typedef struct { QVector3D pos; @@ -140,15 +124,6 @@ class Renderer uint32_t rayBack; } Wall; - /// \brief Renderer-side texture strip (column of square tiles) - typedef struct { - uint32_t * pixels; - uint16_t size; - uint16_t count; - uint16_t mask; - uint32_t block; - } Texture; - /// \brief Bounding-box, half-open ([x0,x1) x [z0,z1)) typedef struct { int x0, x1; @@ -182,12 +157,12 @@ class Renderer /// \brief Frame rendered by the last render() call QImage * getImage() const {return image;} - /// \brief Replace the whole RENDERER_FLAGS bitmask - void setFlags(RENDERER_FLAGS flags); + /// \brief Replace the whole FLAGS bitmask + void setFlags(FLAGS flags); /// \brief Set or clear a single renderer flag - void checkFlag(RENDERER_FLAGS flag, bool checked); - RENDERER_FLAGS getFlags() const {return (RENDERER_FLAGS) flags;} + void checkFlag(FLAGS flag, bool checked); + FLAGS getFlags() const {return (FLAGS) flags;} /// \brief Request a glowmap resize (applied at the start of the next render) void setGlowmapSize(int size); @@ -214,7 +189,7 @@ class Renderer uint16_t clickGetWallID() const {return clickWallID;} // Shared render state (written by Map::pass and Env::pass) - uint32_t flags; ///< active RENDERER_FLAGS bitmask + uint32_t flags; ///< active FLAGS bitmask // Field of view float fovAngle; ///< horizontal FOV, in degrees diff --git a/common/engine/renderer_config.cpp b/common/engine/renderer_config.cpp index 308643c..8c6e9de 100644 --- a/common/engine/renderer_config.cpp +++ b/common/engine/renderer_config.cpp @@ -85,7 +85,7 @@ static int toPowerOfTwo(int v) /*****************************************************************************/ void Renderer::configInit() { - flags = RENDERER_FLAGS_DEFAULT; + flags = FLAGS_DEFAULT; frameResoX = DefaultFrameResoX; frameResoY = DefaultFrameResoY; @@ -173,21 +173,21 @@ bool Renderer::configLoad(const QString & filename) else if (strcmp(key, "GammaKBlue") == 0) gammaKBlue = atof(valueStr); else if (strcmp(key, "Walls") == 0) { - if (atoi(valueStr)) flags |= RENDERER_FLAG_WALLS; else flags &= ~RENDERER_FLAG_WALLS; + if (atoi(valueStr)) flags |= Renderer::FLAG_WALLS; else flags &= ~Renderer::FLAG_WALLS; } else if (strcmp(key, "Surfaces") == 0) { - if (atoi(valueStr)) flags |= RENDERER_FLAG_SURFACES; else flags &= ~RENDERER_FLAG_SURFACES; + if (atoi(valueStr)) flags |= Renderer::FLAG_SURFACES; else flags &= ~Renderer::FLAG_SURFACES; } else if (strcmp(key, "Lights") == 0) { - if (atoi(valueStr)) flags |= RENDERER_FLAG_LIGHTS; else flags &= ~RENDERER_FLAG_LIGHTS; + if (atoi(valueStr)) flags |= Renderer::FLAG_LIGHTS; else flags &= ~Renderer::FLAG_LIGHTS; } else if (strcmp(key, "AmbientOcclusion") == 0) { - if (atoi(valueStr)) flags |= RENDERER_FLAG_AMBIENT_OCCLUSION; else flags &= ~RENDERER_FLAG_AMBIENT_OCCLUSION; + if (atoi(valueStr)) flags |= Renderer::FLAG_AMBIENT_OCCLUSION; else flags &= ~Renderer::FLAG_AMBIENT_OCCLUSION; } else if (strcmp(key, "MotionBlur") == 0) { - if (atoi(valueStr)) flags |= RENDERER_FLAG_MOTIONBLUR; else flags &= ~RENDERER_FLAG_MOTIONBLUR; + if (atoi(valueStr)) flags |= Renderer::FLAG_MOTIONBLUR; else flags &= ~Renderer::FLAG_MOTIONBLUR; } else if (strcmp(key, "Vignette") == 0) { - if (atoi(valueStr)) flags |= RENDERER_FLAG_VIGNETTE; else flags &= ~RENDERER_FLAG_VIGNETTE; + if (atoi(valueStr)) flags |= Renderer::FLAG_VIGNETTE; else flags &= ~Renderer::FLAG_VIGNETTE; } else if (strcmp(key, "Gamma") == 0) { - if (atoi(valueStr)) flags |= RENDERER_FLAG_GAMMA; else flags &= ~RENDERER_FLAG_GAMMA; + if (atoi(valueStr)) flags |= Renderer::FLAG_GAMMA; else flags &= ~Renderer::FLAG_GAMMA; } else if (strcmp(key, "Multithreading") == 0) { - if (atoi(valueStr)) flags |= RENDERER_FLAG_MULTITHREADING; else flags &= ~RENDERER_FLAG_MULTITHREADING; + if (atoi(valueStr)) flags |= Renderer::FLAG_MULTITHREADING; else flags &= ~Renderer::FLAG_MULTITHREADING; } skipLine(file); @@ -245,14 +245,14 @@ bool Renderer::configSave(const QString & filename) // ==== Write render flag bits ==== fprintf(file, "# ==== Render flags ====\n"); - fprintf(file, "Walls = %d\n", (flags & RENDERER_FLAG_WALLS) ? 1 : 0); - fprintf(file, "Surfaces = %d\n", (flags & RENDERER_FLAG_SURFACES) ? 1 : 0); - fprintf(file, "Lights = %d\n", (flags & RENDERER_FLAG_LIGHTS) ? 1 : 0); - fprintf(file, "AmbientOcclusion = %d\n", (flags & RENDERER_FLAG_AMBIENT_OCCLUSION) ? 1 : 0); - fprintf(file, "MotionBlur = %d\n", (flags & RENDERER_FLAG_MOTIONBLUR) ? 1 : 0); - fprintf(file, "Vignette = %d\n", (flags & RENDERER_FLAG_VIGNETTE) ? 1 : 0); - fprintf(file, "Gamma = %d\n", (flags & RENDERER_FLAG_GAMMA) ? 1 : 0); - fprintf(file, "Multithreading = %d\n", (flags & RENDERER_FLAG_MULTITHREADING) ? 1 : 0); + fprintf(file, "Walls = %d\n", (flags & Renderer::FLAG_WALLS) ? 1 : 0); + fprintf(file, "Surfaces = %d\n", (flags & Renderer::FLAG_SURFACES) ? 1 : 0); + fprintf(file, "Lights = %d\n", (flags & Renderer::FLAG_LIGHTS) ? 1 : 0); + fprintf(file, "AmbientOcclusion = %d\n", (flags & Renderer::FLAG_AMBIENT_OCCLUSION) ? 1 : 0); + fprintf(file, "MotionBlur = %d\n", (flags & Renderer::FLAG_MOTIONBLUR) ? 1 : 0); + fprintf(file, "Vignette = %d\n", (flags & Renderer::FLAG_VIGNETTE) ? 1 : 0); + fprintf(file, "Gamma = %d\n", (flags & Renderer::FLAG_GAMMA) ? 1 : 0); + fprintf(file, "Multithreading = %d\n", (flags & Renderer::FLAG_MULTITHREADING) ? 1 : 0); fprintf(file, "\n"); fclose(file); diff --git a/editor/mainwindow.cpp b/editor/mainwindow.cpp index 4fb6731..26b6622 100644 --- a/editor/mainwindow.cpp +++ b/editor/mainwindow.cpp @@ -226,7 +226,7 @@ void MainWindow::applyUndoState() updateSunProperties(); updateFogProperties(); updateUndoActions(); - renderer.flags |= RENDERER_FLAG_GLOWMAP_REBUILD; + renderer.flags |= Renderer::FLAG_GLOWMAP_REBUILD; } void MainWindow::updateUndoActions() @@ -886,16 +886,16 @@ void MainWindow::updateEditorProperties() void MainWindow::updateEngineProperties() { - RENDERER_FLAGS flags = renderer.getFlags(); - setCheckboxStateSilently(ui->checkRendererMultithreadingEnable, flags & RENDERER_FLAG_MULTITHREADING); - setCheckboxStateSilently(ui->checkRendererWallsEnable, flags & RENDERER_FLAG_WALLS); - setCheckboxStateSilently(ui->checkRendererSurfacesEnable, flags & RENDERER_FLAG_SURFACES); - setCheckboxStateSilently(ui->checkRendererLightsEnable, flags & RENDERER_FLAG_LIGHTS); - setCheckboxStateSilently(ui->checkRendererOcclusionEnable, flags & RENDERER_FLAG_AMBIENT_OCCLUSION); - setCheckboxStateSilently(ui->checkRendererMotionblur, flags & RENDERER_FLAG_MOTIONBLUR); - setCheckboxStateSilently(ui->checkRendererVignetting, flags & RENDERER_FLAG_VIGNETTE); - setCheckboxStateSilently(ui->checkRendererAlphaFeatures, flags & RENDERER_FLAG_ALPHA_FEATURES); - setCheckboxStateSilently(ui->checkRendererGamma, flags & RENDERER_FLAG_GAMMA); + Renderer::FLAGS flags = renderer.getFlags(); + setCheckboxStateSilently(ui->checkRendererMultithreadingEnable, flags & Renderer::FLAG_MULTITHREADING); + setCheckboxStateSilently(ui->checkRendererWallsEnable, flags & Renderer::FLAG_WALLS); + setCheckboxStateSilently(ui->checkRendererSurfacesEnable, flags & Renderer::FLAG_SURFACES); + setCheckboxStateSilently(ui->checkRendererLightsEnable, flags & Renderer::FLAG_LIGHTS); + setCheckboxStateSilently(ui->checkRendererOcclusionEnable, flags & Renderer::FLAG_AMBIENT_OCCLUSION); + setCheckboxStateSilently(ui->checkRendererMotionblur, flags & Renderer::FLAG_MOTIONBLUR); + setCheckboxStateSilently(ui->checkRendererVignetting, flags & Renderer::FLAG_VIGNETTE); + setCheckboxStateSilently(ui->checkRendererAlphaFeatures, flags & Renderer::FLAG_ALPHA_FEATURES); + setCheckboxStateSilently(ui->checkRendererGamma, flags & Renderer::FLAG_GAMMA); ui->scrollRendererMotionblurPercent->blockSignals(true); ui->scrollRendererMotionblurPercent->setValue((int)renderer.motionBlurFactor); @@ -1078,7 +1078,7 @@ void MainWindow::on_pushNodeAddLight_clicked() return; setEditMode(EDIT_MODE_LIGHTS); editor.selectedLight = lId; - renderer.flags |= RENDERER_FLAG_GLOWMAP_REBUILD; + renderer.flags |= Renderer::FLAG_GLOWMAP_REBUILD; updateLightProperties(); } @@ -1450,7 +1450,7 @@ void MainWindow::on_checkSpriteShadows_toggled(bool checked) Sprite & b = editor.editedMap->sprites[editor.selectedSprite]; b.flags &= ~SPRITE_FLAG_SHADOWS; if (checked) b.flags |= SPRITE_FLAG_SHADOWS; - renderer.flags |= RENDERER_FLAG_GLOWMAP_REBUILD; + renderer.flags |= Renderer::FLAG_GLOWMAP_REBUILD; } void MainWindow::on_checkSpriteAutopan_toggled(bool checked) @@ -1915,7 +1915,7 @@ void MainWindow::on_pushLightColorA_clicked() if (!l.selected) continue; l.colorA = color; } - renderer.flags |= RENDERER_FLAG_GLOWMAP_REBUILD; + renderer.flags |= Renderer::FLAG_GLOWMAP_REBUILD; } void MainWindow::on_pushLightColorB_clicked() @@ -1933,7 +1933,7 @@ void MainWindow::on_pushLightColorB_clicked() if (!l.selected) continue; l.colorB = color; } - renderer.flags |= RENDERER_FLAG_GLOWMAP_REBUILD; + renderer.flags |= Renderer::FLAG_GLOWMAP_REBUILD; } void MainWindow::on_scrollLightStrength_valueChanged(int value) @@ -1945,7 +1945,7 @@ void MainWindow::on_scrollLightStrength_valueChanged(int value) if (!l.selected) continue; l.strength = value * 0.03125f; } - renderer.flags |= RENDERER_FLAG_GLOWMAP_REBUILD; + renderer.flags |= Renderer::FLAG_GLOWMAP_REBUILD; } void MainWindow::on_spinLightFalloff_valueChanged(double value) @@ -1957,7 +1957,7 @@ void MainWindow::on_spinLightFalloff_valueChanged(double value) if (!l.selected) continue; l.falloff = (float) value; } - renderer.flags |= RENDERER_FLAG_GLOWMAP_REBUILD; + renderer.flags |= Renderer::FLAG_GLOWMAP_REBUILD; } void MainWindow::on_comboLightAnimation_currentIndexChanged(int index) @@ -1969,7 +1969,7 @@ void MainWindow::on_comboLightAnimation_currentIndexChanged(int index) if (!l.selected) continue; l.anim = index; } - renderer.flags |= RENDERER_FLAG_GLOWMAP_REBUILD; + renderer.flags |= Renderer::FLAG_GLOWMAP_REBUILD; } void MainWindow::on_scrollLightSpeed_valueChanged(int value) @@ -1994,7 +1994,7 @@ void MainWindow::on_checkLightEnable_toggled(bool checked) l.flags &= ~LIGHT_FLAG_ENABLE; if (checked) l.flags |= LIGHT_FLAG_ENABLE; } - renderer.flags |= RENDERER_FLAG_GLOWMAP_REBUILD; + renderer.flags |= Renderer::FLAG_GLOWMAP_REBUILD; } void MainWindow::on_comboLightTag_currentIndexChanged(int index) @@ -2013,7 +2013,7 @@ void MainWindow::on_pushLightDelete_clicked() editor.lightDelete(i--); } editor.selectedLight = EDIT_UNSELECTED; - renderer.flags |= RENDERER_FLAG_GLOWMAP_REBUILD; + renderer.flags |= Renderer::FLAG_GLOWMAP_REBUILD; updateLightProperties(); } @@ -2692,7 +2692,7 @@ void MainWindow::on_actionNew_triggered() { renderer.init(); editor.rootMap.init(); - renderer.flags |= RENDERER_FLAG_GLOWMAP_REBUILD; + renderer.flags |= Renderer::FLAG_GLOWMAP_REBUILD; undoDebounceTimer->stop(); undoHistory.clear(); @@ -2713,7 +2713,7 @@ void MainWindow::on_actionLoad_map_triggered() updateFogProperties(); updateTagProperties(); refreshTagCombos(); - renderer.flags |= RENDERER_FLAG_GLOWMAP_REBUILD; + renderer.flags |= Renderer::FLAG_GLOWMAP_REBUILD; undoDebounceTimer->stop(); undoHistory.clear(); @@ -2825,42 +2825,42 @@ void MainWindow::on_checkEditorWallSelector_toggled(bool checked) void MainWindow::on_checkRendererMultithreadingEnable_toggled(bool checked) { - renderer.checkFlag(RENDERER_FLAG_MULTITHREADING, checked); + renderer.checkFlag(Renderer::FLAG_MULTITHREADING, checked); } void MainWindow::on_checkRendererWallsEnable_toggled(bool checked) { - renderer.checkFlag(RENDERER_FLAG_WALLS, checked); + renderer.checkFlag(Renderer::FLAG_WALLS, checked); } void MainWindow::on_checkRendererSurfacesEnable_toggled(bool checked) { - renderer.checkFlag(RENDERER_FLAG_SURFACES, checked); + renderer.checkFlag(Renderer::FLAG_SURFACES, checked); } void MainWindow::on_checkRendererOcclusionEnable_toggled(bool checked) { - renderer.checkFlag(RENDERER_FLAG_AMBIENT_OCCLUSION, checked); + renderer.checkFlag(Renderer::FLAG_AMBIENT_OCCLUSION, checked); } void MainWindow::on_checkRendererLightsEnable_toggled(bool checked) { - renderer.checkFlag(RENDERER_FLAG_LIGHTS, checked); + renderer.checkFlag(Renderer::FLAG_LIGHTS, checked); } void MainWindow::on_checkRendererMotionblur_toggled(bool checked) { - renderer.checkFlag(RENDERER_FLAG_MOTIONBLUR, checked); + renderer.checkFlag(Renderer::FLAG_MOTIONBLUR, checked); } void MainWindow::on_checkRendererVignetting_toggled(bool checked) { - renderer.checkFlag(RENDERER_FLAG_VIGNETTE, checked); + renderer.checkFlag(Renderer::FLAG_VIGNETTE, checked); } void MainWindow::on_checkRendererAlphaFeatures_toggled(bool checked) { - renderer.checkFlag(RENDERER_FLAG_ALPHA_FEATURES, checked); + renderer.checkFlag(Renderer::FLAG_ALPHA_FEATURES, checked); } void MainWindow::on_spinRendererOcclusionLength_valueChanged(double arg1) @@ -2891,7 +2891,7 @@ void MainWindow::on_scrollRendererVignettingOuter_valueChanged(int value) void MainWindow::on_checkRendererGamma_toggled(bool checked) { - renderer.checkFlag(RENDERER_FLAG_GAMMA, checked); + renderer.checkFlag(Renderer::FLAG_GAMMA, checked); } void MainWindow::on_scrollRendererGammaKRed_valueChanged(int value) @@ -3102,7 +3102,7 @@ void MainWindow::on_comboGlowmapSize_currentIndexChanged(int index) void MainWindow::on_pushRendererGlowmapRebuild_clicked() { - renderer.flags |= RENDERER_FLAG_GLOWMAP_REBUILD; + renderer.flags |= Renderer::FLAG_GLOWMAP_REBUILD; } /*****************************************************************************/ diff --git a/editor/mainwindow.ui b/editor/mainwindow.ui index 06cb6b1..75cd5b0 100644 --- a/editor/mainwindow.ui +++ b/editor/mainwindow.ui @@ -560,11 +560,11 @@ QTabWidget::TabShape::Rounded - 10 + 0 - + :/Icons/tools-node.png:/Icons/tools-node.png @@ -949,7 +949,7 @@ QAbstractItemView::SelectionMode::ExtendedSelection - + 10 @@ -965,7 +965,7 @@ - + :/Icons/tools-wall.png:/Icons/tools-wall.png @@ -1396,7 +1396,7 @@ - + :/Icons/tools-submap.png:/Icons/tools-submap.png @@ -1625,7 +1625,7 @@ - + :/Icons/tools-staircase.png:/Icons/tools-staircase.png @@ -1954,7 +1954,7 @@ - + :/Icons/tools-door.png:/Icons/tools-door.png @@ -2491,7 +2491,7 @@ - + :/Icons/tools-lift.png:/Icons/tools-lift.png @@ -3038,7 +3038,7 @@ - + :/Icons/tools-sprite.png:/Icons/tools-sprite.png @@ -3347,7 +3347,7 @@ - + :/Icons/tools-light.png:/Icons/tools-light.png @@ -3653,7 +3653,7 @@ - + :/Icons/tools-speaker.png:/Icons/tools-speaker.png @@ -4477,7 +4477,7 @@ - + :/Icons/tools-stage.png:/Icons/tools-stage.png @@ -4708,7 +4708,7 @@ - + :/Icons/tools-settings.png:/Icons/tools-settings.png @@ -5257,7 +5257,7 @@ - + :/Icons/tools-origin.png:/Icons/tools-origin.png @@ -5460,7 +5460,7 @@ 0 0 1140 - 23 + 25 @@ -5505,7 +5505,7 @@ - New + New map Ctrl+N @@ -5651,8 +5651,6 @@ 1 - - - + diff --git a/editor/waller.pro b/editor/waller.pro index 74fcd2e..19b6ee4 100644 --- a/editor/waller.pro +++ b/editor/waller.pro @@ -39,6 +39,7 @@ SOURCES += \ ../common/engine/gamepad.cpp HEADERS += \ + ../common/engine/primitives.h \ globals.h \ editor.h \ walker.h \ diff --git a/editor/wdgmapeditor.cpp b/editor/wdgmapeditor.cpp index 508716a..a96a5b8 100644 --- a/editor/wdgmapeditor.cpp +++ b/editor/wdgmapeditor.cpp @@ -659,7 +659,7 @@ void WdgMapEditor::mouseMoveDrag(QMouseEvent *event) }else if (editor.editMode == EDIT_MODE_LIGHTS) { if (dragNodeItems(editor.editedMap->lights, editor.selectedLight, [](const Light & l){ return l.nodeID; })) { - renderer.flags |= RENDERER_FLAG_GLOWMAP_REBUILD; + renderer.flags |= Renderer::FLAG_GLOWMAP_REBUILD; mainWindow->updateLightProperties(); } From a5313b5e45903033a7991b5b7f1f496c1c171fe3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Meslin?= Date: Sat, 27 Jun 2026 19:55:23 +0200 Subject: [PATCH 3/3] Add bone editing, flesh rendering, and texture/frame panels to Rigger Bones: connect joints into bones, select/move/delete, rectangle-select, billboard quad with width/length/offset/minWidth, mirror and rotate flags, per-view-arc images (Front/Right/Back/Left), and a swap button for the two joints. Flesh: render the bones as textured billboard quads via the shared drawer; extract the rendering loop into Rigger::renderFlesh, plus Rigger::renderAnimationFrame for a bounding-box-fitted, frame-interpolated bake of a single animation pose into an arbitrary image. UI: a texture-strip selector (WdgTexSelector) and per-bone texture preview (WdgTexView) for assigning images, a frame-card selector (WdgFrameSelector) for picking/adding/deleting/reordering animation frames, joint/bone property panels, display toggles (joints/bones/flesh) and editAllFrames, viewer pan/diameter/y controls, J/B/Esc shortcuts, and lastrig.rig session persistence. --- animator/CMakeLists.txt | 4 + animator/mainwindow.cpp | 357 +++++++++++++++++++++++++++++++++- animator/mainwindow.h | 51 +++++ animator/mainwindow.ui | 247 +++++++++++++++++++---- animator/rigger.cpp | 189 ++++++++++++++++-- animator/rigger.h | 53 +++-- animator/rigger.pro | 13 +- animator/wdgframeselector.cpp | 109 +++++++++++ animator/wdgframeselector.h | 36 ++++ animator/wdgrigeditor.cpp | 180 +++++++++++++---- animator/wdgrigeditor.h | 1 + animator/wdgtexselector.cpp | 107 ++++++++++ animator/wdgtexselector.h | 36 ++++ animator/wdgtexview.cpp | 45 +++++ animator/wdgtexview.h | 34 ++++ common/engine/rig_io.cpp | 10 +- common/engine/rigobjects.h | 8 +- 17 files changed, 1353 insertions(+), 127 deletions(-) create mode 100644 animator/wdgframeselector.cpp create mode 100644 animator/wdgframeselector.h create mode 100644 animator/wdgtexselector.cpp create mode 100644 animator/wdgtexselector.h create mode 100644 animator/wdgtexview.cpp create mode 100644 animator/wdgtexview.h diff --git a/animator/CMakeLists.txt b/animator/CMakeLists.txt index e63bad1..ca4749c 100644 --- a/animator/CMakeLists.txt +++ b/animator/CMakeLists.txt @@ -15,8 +15,12 @@ set(SOURCES rigger.cpp mainwindow.cpp wdgrigeditor.cpp + wdgtexselector.cpp + wdgframeselector.cpp + wdgtexview.cpp ../common/engine/rig.cpp ../common/engine/rig_io.cpp + ../common/engine/drawer.cpp mainwindow.ui ) diff --git a/animator/mainwindow.cpp b/animator/mainwindow.cpp index bc84997..a06f14c 100644 --- a/animator/mainwindow.cpp +++ b/animator/mainwindow.cpp @@ -11,14 +11,52 @@ #include #include +#include + MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) , ui(new Ui::MainWindow) { ui->setupUi(this); resizeUI(); + rigger.mode = (RIG_MODES) ui->tabRiggerModes->currentIndex(); + + setCheckboxStateSilently(ui->checkDisplayJoints, rigger.flags & FLAG_DISPLAY_JOINTS); + setCheckboxStateSilently(ui->checkDisplayBones, rigger.flags & FLAG_DISPLAY_BONES); + setCheckboxStateSilently(ui->checkDisplayFlesh, rigger.flags & FLAG_DISPLAY_FLESH); + ui->pushAllFrames->blockSignals(true); + ui->pushAllFrames->setChecked(rigger.editAllFrames); + ui->pushAllFrames->blockSignals(false); + updateJointProperties(); + updateBoneProperties(); updateViewerProperties(); + + createShortcuts(); +} + +/*****************************************************************************/ +void MainWindow::createShortcuts() +{ + shortcutJoints = new QShortcut(QKeySequence(Qt::Key_J), this); + connect(shortcutJoints, SIGNAL(activated()), this, SLOT(on_setJointMode())); + + shortcutBones = new QShortcut(QKeySequence(Qt::Key_B), this); + connect(shortcutBones, SIGNAL(activated()), this, SLOT(on_setBoneMode())); + + shortcutDeselect = new QShortcut(QKeySequence(Qt::Key_Escape), this); + connect(shortcutDeselect, SIGNAL(activated()), this, SLOT(on_deselect())); +} + +void MainWindow::on_setJointMode() { ui->tabRiggerModes->setCurrentIndex(RIG_MODE_JOINTS); } +void MainWindow::on_setBoneMode() { ui->tabRiggerModes->setCurrentIndex(RIG_MODE_BONES); } + +void MainWindow::on_deselect() +{ + rigger.deselect(); + updateJointProperties(); + updateBoneProperties(); + ui->widgetRig->update(); } MainWindow::~MainWindow() @@ -54,17 +92,19 @@ void MainWindow::resizeUI() ui->groupAnimate->setGeometry(rightX, 220, rightW, 151); ui->comboAnimationName->setGeometry(rightX, 380, rightW, 22); -// Frame buttons (2x2), anchored to the bottom, left of the right column - const int btnW = 71, btnH = 23, gap = 8, rowGap = 7; - int blockW = 2 * btnW + gap; +// Frame buttons (3 columns x 2 rows), anchored to the bottom, left of the right column + const int btnW = 71, btnH = 23, gap = 8, rowGap = 7, moveW = 51; + int blockW = 2 * btnW + 2 * gap + moveW; int bottomH = framesH + 4 + scrollH; int framesTop = ch - margin - bottomH; int blockX = rightX - margin - blockW; ui->pushFrameAdd->setGeometry(blockX, framesTop, btnW, btnH); - ui->pushFrameDel->setGeometry(blockX, framesTop + btnH + rowGap, btnW, btnH); + ui->pushFrameDelete->setGeometry(blockX, framesTop + btnH + rowGap, btnW, btnH); ui->pushFrameCopy->setGeometry(blockX + btnW + gap, framesTop, btnW, btnH); ui->pushFramePaste->setGeometry(blockX + btnW + gap, framesTop + btnH + rowGap, btnW, btnH); + ui->frameMoveRight->setGeometry(blockX + 2 * btnW + 2 * gap, framesTop, moveW, btnH); + ui->pushFrameMoveLeft->setGeometry(blockX + 2 * btnW + 2 * gap, framesTop + btnH + rowGap, moveW, btnH); // Frames timeline + scrollbar fill the rest of the bottom strip int framesX = leftW + margin; @@ -73,12 +113,20 @@ void MainWindow::resizeUI() ui->widgetFrames->setGeometry(framesX, framesTop, framesW, framesH); ui->scrollFrames->setGeometry(framesX, framesTop + framesH + 4, framesW, scrollH); -// Central canvas fills the area between the panels +// Texture strip selector + scrollbar, just above the frames block + const int texSelH = 50, texScrollH = 16; int canvasX = leftW + margin; - int canvasY = margin; int canvasW = rightX - margin - canvasX; - int canvasH = framesTop - margin - canvasY; if (canvasW < 1) canvasW = 1; + + int texTop = framesTop - 6 - (texSelH + 4 + texScrollH); + ui->widgetTextureSelector->setGeometry(canvasX, texTop, canvasW, texSelH); + ui->scrollTextures->setGeometry(canvasX, texTop + texSelH + 4, canvasW, texScrollH); + ui->pushTextureBrowse->setGeometry(canvasW - 31, (texSelH - 21) / 2, 21, 21); + +// Central canvas fills the area between the panels and the texture block + int canvasY = margin; + int canvasH = texTop - margin - canvasY; if (canvasH < 1) canvasH = 1; ui->widgetRig->setGeometry(canvasX, canvasY, canvasW, canvasH); } @@ -98,6 +146,20 @@ void MainWindow::setSpinValueSilently(QAbstractSpinBox * box, double value) } } +void MainWindow::setCheckboxStateSilently(QCheckBox * box, bool checked) +{ + box->blockSignals(true); + box->setChecked(checked); + box->blockSignals(false); +} + +/*****************************************************************************/ +void MainWindow::on_tabRiggerModes_currentChanged(int index) +{ + rigger.mode = (RIG_MODES) index; + ui->widgetRig->update(); +} + /*****************************************************************************/ void MainWindow::updateJointProperties() { @@ -202,6 +264,280 @@ void MainWindow::on_pushJointDelete_clicked() ui->widgetRig->update(); } +/*****************************************************************************/ +void MainWindow::updateBoneProperties() +{ + int count = rigger.rig.bones.count(); + + if (rigger.selectedBone < 0 || rigger.selectedBone >= count) { + ui->plainBoneID->setPlainText("None"); + setSpinValueSilently(ui->spinBoneWidth, 0.0); + setSpinValueSilently(ui->spinBoneLength, 0.0); + setSpinValueSilently(ui->spinBoneOffset, 0.0); + setSpinValueSilently(ui->spinBoneMinimumWidth, 0.0); + setCheckboxStateSilently(ui->checkBoneInvisible, false); + setCheckboxStateSilently(ui->checkBoneMirrored, false); + setCheckboxStateSilently(ui->checkBoneRotate, false); + setSpinValueSilently(ui->spinBoneTexture, 0.0); + ui->widgetBoneTexture->setID(0); + return; + } + + Bone & b = rigger.rig.bones[rigger.selectedBone]; + int arc = ui->comboBonePicture->currentIndex(); + if (arc < 0) arc = 0; + + ui->plainBoneID->setPlainText(QString::number(rigger.selectedBone)); + setSpinValueSilently(ui->spinBoneWidth, b.width); + setSpinValueSilently(ui->spinBoneLength, b.length); + setSpinValueSilently(ui->spinBoneOffset, b.offset); + setSpinValueSilently(ui->spinBoneMinimumWidth, b.minWidth); + setCheckboxStateSilently(ui->checkBoneInvisible, b.flags & BONE_FLAG_INVISIBLE); + setCheckboxStateSilently(ui->checkBoneMirrored, b.flags & BONE_FLAG_MIRROR); + setCheckboxStateSilently(ui->checkBoneRotate, b.flags & BONE_FLAG_ROTATE); + setSpinValueSilently(ui->spinBoneTexture, b.images[arc]); + ui->widgetBoneTexture->setID(b.images[arc]); +} + +void MainWindow::on_spinBoneWidth_valueChanged(double arg1) +{ + if (rigger.selectedBone < 0) return; + for (Bone & b : rigger.rig.bones) { + if (!b.selected) continue; + b.width = arg1; + } + ui->widgetRig->update(); +} + +void MainWindow::on_spinBoneLength_valueChanged(double arg1) +{ + if (rigger.selectedBone < 0) return; + for (Bone & b : rigger.rig.bones) { + if (!b.selected) continue; + b.length = arg1; + } + ui->widgetRig->update(); +} + +void MainWindow::on_spinBoneOffset_valueChanged(double arg1) +{ + if (rigger.selectedBone < 0) return; + for (Bone & b : rigger.rig.bones) { + if (!b.selected) continue; + b.offset = arg1; + } + ui->widgetRig->update(); +} + +void MainWindow::on_spinBoneMinimumWidth_valueChanged(double arg1) +{ + if (rigger.selectedBone < 0) return; + for (Bone & b : rigger.rig.bones) { + if (!b.selected) continue; + b.minWidth = arg1; + } + ui->widgetRig->update(); +} + +void MainWindow::on_checkBoneInvisible_toggled(bool checked) +{ + if (rigger.selectedBone < 0) return; + for (Bone & b : rigger.rig.bones) { + if (!b.selected) continue; + if (checked) b.flags |= BONE_FLAG_INVISIBLE; + else b.flags &= ~BONE_FLAG_INVISIBLE; + } + ui->widgetRig->update(); +} + +void MainWindow::on_checkBoneMirrored_toggled(bool checked) +{ + if (rigger.selectedBone < 0) return; + for (Bone & b : rigger.rig.bones) { + if (!b.selected) continue; + if (checked) b.flags |= BONE_FLAG_MIRROR; + else b.flags &= ~BONE_FLAG_MIRROR; + } + ui->widgetRig->update(); +} + +void MainWindow::on_checkBoneRotate_toggled(bool checked) +{ + if (rigger.selectedBone < 0) return; + for (Bone & b : rigger.rig.bones) { + if (!b.selected) continue; + if (checked) b.flags |= BONE_FLAG_ROTATE; + else b.flags &= ~BONE_FLAG_ROTATE; + } + ui->widgetRig->update(); +} + +void MainWindow::on_comboBonePicture_currentIndexChanged(int index) +{ +// Show the image assigned to the newly selected arc + if (rigger.selectedBone < 0) { + setSpinValueSilently(ui->spinBoneTexture, 0.0); + ui->widgetBoneTexture->setID(0); + return; + } + int arc = index < 0 ? 0 : index; + Bone & b = rigger.rig.bones[rigger.selectedBone]; + setSpinValueSilently(ui->spinBoneTexture, b.images[arc]); + ui->widgetBoneTexture->setID(b.images[arc]); + rigger.selectedTextureID = b.images[arc]; + ui->widgetTextureSelector->update(); +} + +void MainWindow::on_spinBoneTexture_valueChanged(int arg1) +{ + if (rigger.selectedBone < 0) return; + int arc = ui->comboBonePicture->currentIndex(); + if (arc < 0) arc = 0; + for (Bone & b : rigger.rig.bones) { + if (!b.selected) continue; + b.images[arc] = (uint16_t) arg1; + if (b.imageCount < arc + 1) b.imageCount = (uint16_t)(arc + 1); + } + ui->widgetBoneTexture->setID((uint16_t) arg1); + ui->widgetRig->update(); +} + +void MainWindow::on_pushBoneDelete_clicked() +{ + if (rigger.selectedBone < 0) return; + rigger.boneDelete(rigger.selectedBone); + updateBoneProperties(); + ui->widgetRig->update(); +} + +void MainWindow::on_pushBoneSwap_clicked() +{ + if (rigger.selectedBone < 0) return; + for (Bone & b : rigger.rig.bones) { + if (!b.selected) continue; + std::swap(b.jointID1, b.jointID2); + } + ui->widgetRig->update(); +} + +/*****************************************************************************/ +void MainWindow::setTexture(uint16_t texId) +{ + rigger.selectedTextureID = texId; + if (rigger.selectedBone < 0) return; + + int arc = ui->comboBonePicture->currentIndex(); + if (arc < 0) arc = 0; + + for (Bone & b : rigger.rig.bones) { + if (!b.selected) continue; + b.images[arc] = texId; + if (b.imageCount < arc + 1) b.imageCount = (uint16_t)(arc + 1); + } + setSpinValueSilently(ui->spinBoneTexture, texId); + ui->widgetBoneTexture->setID(texId); + ui->widgetRig->update(); +} + +void MainWindow::on_pushTextureBrowse_clicked() +{ + QString path = QDir::currentPath(); + QString file = QFileDialog::getOpenFileName(this, "Open texture strip", path, "Image File (*.bmp *.png *.jpg)"); + if (file.isEmpty()) return; + + rigger.rig.textures.load(file); + rigger.selectedTextureID = 0; + ui->widgetTextureSelector->setScroll(0); + ui->widgetTextureSelector->update(); +} + +void MainWindow::on_scrollTextures_valueChanged(int value) +{ + ui->widgetTextureSelector->setScroll(value); +} + +/*****************************************************************************/ +void MainWindow::updateRigCanvas() +{ + ui->widgetRig->update(); +} + +void MainWindow::on_pushFrameAdd_clicked() +{ + rigger.rig.frameInsert(rigger.rig.currentFrame + 1); + updateJointProperties(); + ui->widgetFrames->update(); + ui->widgetRig->update(); +} + +void MainWindow::on_pushFrameDelete_clicked() +{ + rigger.rig.frameDelete(rigger.rig.currentFrame); + updateJointProperties(); + ui->widgetFrames->update(); + ui->widgetRig->update(); +} + +void MainWindow::on_scrollFrames_valueChanged(int value) +{ + ui->widgetFrames->setScroll(value); +} + +void MainWindow::on_frameMoveRight_clicked() +{ + Animation * a = rigger.rig.currentAnimationPtr(); + if (!a) return; + int cur = rigger.rig.currentFrame; + if (cur < 0 || cur >= a->frames.count() - 1) return; + + a->frames.swapItemsAt(cur, cur + 1); + rigger.rig.frameSelect(cur + 1); + updateJointProperties(); + ui->widgetFrames->update(); + ui->widgetRig->update(); +} + +void MainWindow::on_pushFrameMoveLeft_clicked() +{ + Animation * a = rigger.rig.currentAnimationPtr(); + if (!a) return; + int cur = rigger.rig.currentFrame; + if (cur <= 0 || cur >= a->frames.count()) return; + + a->frames.swapItemsAt(cur, cur - 1); + rigger.rig.frameSelect(cur - 1); + updateJointProperties(); + ui->widgetFrames->update(); + ui->widgetRig->update(); +} + +/*****************************************************************************/ +void MainWindow::on_checkDisplayJoints_toggled(bool checked) +{ + if (checked) rigger.flags |= FLAG_DISPLAY_JOINTS; + else rigger.flags &= ~FLAG_DISPLAY_JOINTS; + ui->widgetRig->update(); +} + +void MainWindow::on_checkDisplayBones_toggled(bool checked) +{ + if (checked) rigger.flags |= FLAG_DISPLAY_BONES; + else rigger.flags &= ~FLAG_DISPLAY_BONES; + ui->widgetRig->update(); +} + +void MainWindow::on_checkDisplayFlesh_toggled(bool checked) +{ + if (checked) rigger.flags |= FLAG_DISPLAY_FLESH; + else rigger.flags &= ~FLAG_DISPLAY_FLESH; + ui->widgetRig->update(); +} + +void MainWindow::on_pushAllFrames_toggled(bool checked) +{ + rigger.editAllFrames = checked; +} + /*****************************************************************************/ void MainWindow::updateViewerProperties() { @@ -232,9 +568,13 @@ void MainWindow::on_spinViewerZ_valueChanged(double arg1) void MainWindow::on_actionNew_triggered() { rigger.init(); + rigger.mode = (RIG_MODES) ui->tabRiggerModes->currentIndex(); updateJointProperties(); + updateBoneProperties(); updateViewerProperties(); ui->widgetRig->update(); + ui->widgetTextureSelector->update(); + ui->widgetFrames->update(); setWindowTitle("Rigger"); } @@ -247,7 +587,10 @@ void MainWindow::on_actionLoad_triggered() rigger.rig.load(file); rigger.deselect(); updateJointProperties(); + updateBoneProperties(); ui->widgetRig->update(); + ui->widgetTextureSelector->update(); + ui->widgetFrames->update(); setWindowTitle("Rigger : " + QFileInfo(file).fileName()); } diff --git a/animator/mainwindow.h b/animator/mainwindow.h index 0fd43c9..9f76af9 100644 --- a/animator/mainwindow.h +++ b/animator/mainwindow.h @@ -3,6 +3,10 @@ #include #include +#include +#include + +#include namespace Ui { class MainWindow; @@ -19,25 +23,72 @@ class MainWindow : public QMainWindow /// \brief Refresh the joint list and the selected joint's properties void updateJointProperties(); + /// \brief Refresh the selected bone's properties + void updateBoneProperties(); + /// \brief Refresh the viewer controls from the rig view void updateViewerProperties(); + /// \brief Assign the selected texture to the current bone arc (image) + void setTexture(uint16_t texId); + + /// \brief Repaint the rig canvas (e.g. after the current frame changes) + void updateRigCanvas(); + protected: void resizeEvent(QResizeEvent *event) override; private: void resizeUI(); + void createShortcuts(); void setSpinValueSilently(QAbstractSpinBox * box, double value); + void setCheckboxStateSilently(QCheckBox * box, bool checked); + + QShortcut * shortcutJoints; + QShortcut * shortcutBones; + QShortcut * shortcutDeselect; Ui::MainWindow *ui; private slots: + void on_setJointMode(); + void on_setBoneMode(); + void on_deselect(); + + void on_tabRiggerModes_currentChanged(int index); + void on_spinJointX_valueChanged(double arg1); void on_spinJointY_valueChanged(double arg1); void on_spinJointZ_valueChanged(double arg1); void on_listJoints_itemSelectionChanged(); void on_pushJointDelete_clicked(); + void on_spinBoneWidth_valueChanged(double arg1); + void on_spinBoneLength_valueChanged(double arg1); + void on_spinBoneOffset_valueChanged(double arg1); + void on_spinBoneMinimumWidth_valueChanged(double arg1); + void on_checkBoneInvisible_toggled(bool checked); + void on_checkBoneMirrored_toggled(bool checked); + void on_checkBoneRotate_toggled(bool checked); + void on_comboBonePicture_currentIndexChanged(int index); + void on_spinBoneTexture_valueChanged(int arg1); + void on_pushBoneDelete_clicked(); + void on_pushBoneSwap_clicked(); + + void on_pushTextureBrowse_clicked(); + void on_scrollTextures_valueChanged(int value); + + void on_pushFrameAdd_clicked(); + void on_pushFrameDelete_clicked(); + void on_scrollFrames_valueChanged(int value); + void on_frameMoveRight_clicked(); + void on_pushFrameMoveLeft_clicked(); + + void on_checkDisplayJoints_toggled(bool checked); + void on_checkDisplayBones_toggled(bool checked); + void on_checkDisplayFlesh_toggled(bool checked); + void on_pushAllFrames_toggled(bool checked); + void on_spinViewerPan_valueChanged(double arg1); void on_spinViewerY_valueChanged(double arg1); void on_spinViewerZ_valueChanged(double arg1); diff --git a/animator/mainwindow.ui b/animator/mainwindow.ui index eb35925..834c61e 100644 --- a/animator/mainwindow.ui +++ b/animator/mainwindow.ui @@ -7,7 +7,7 @@ 0 0 900 - 490 + 556 @@ -26,7 +26,7 @@ 0 0 201 - 451 + 521 @@ -245,7 +245,7 @@ 11 - 220 + 230 151 22 @@ -271,11 +271,11 @@ - + 50 - 250 + 260 64 64 @@ -285,7 +285,7 @@ 100 - 410 + 370 61 23 @@ -349,7 +349,7 @@ 11 - 320 + 330 151 22 @@ -457,6 +457,77 @@ Offset: + + + + 70 + 180 + 91 + 20 + + + + mirrored + + + + + + 70 + 200 + 91 + 20 + + + + rotate + + + + + + 70 + 130 + 91 + 22 + + + + -1024.000000000000000 + + + 1024.000000000000000 + + + 0.250000000000000 + + + + + + 10 + 130 + 61 + 21 + + + + MinW: + + + + + + 10 + 370 + 61 + 23 + + + + Swap + + @@ -530,7 +601,7 @@ - + 210 @@ -545,7 +616,7 @@ 580 360 - 71 + 51 23 @@ -553,12 +624,12 @@ Add - + 580 390 - 71 + 51 23 @@ -569,9 +640,9 @@ - 660 + 630 360 - 71 + 51 23 @@ -582,9 +653,9 @@ - 660 + 630 390 - 71 + 51 23 @@ -698,7 +769,7 @@ 740 220 151 - 151 + 221 @@ -708,7 +779,7 @@ 10 - 60 + 90 51 21 @@ -721,7 +792,7 @@ 10 - 30 + 60 51 21 @@ -734,7 +805,7 @@ 10 - 90 + 120 51 21 @@ -747,7 +818,7 @@ 60 - 90 + 120 81 22 @@ -763,7 +834,7 @@ 60 - 30 + 60 81 22 @@ -773,23 +844,51 @@ 60 - 60 + 90 81 22 - + 10 - 120 + 190 131 21 - Run + Stop + + + + + + 10 + 30 + 131 + 22 + + + + + Idle + + + + + + + 10 + 160 + 131 + 21 + + + + Start @@ -806,21 +905,6 @@ Qt::Orientation::Horizontal - - - - 740 - 380 - 151 - 22 - - - - - Default - - - @@ -856,6 +940,71 @@ + + + + 210 + 500 + 521 + 16 + + + + 999 + + + Qt::Orientation::Horizontal + + + + + + 210 + 450 + 521 + 51 + + + + + + 490 + 20 + 21 + 21 + + + + ... + + + + + + + 680 + 360 + 51 + 23 + + + + => + + + + + + 680 + 390 + 51 + 23 + + + + <= + + @@ -931,12 +1080,30 @@ + + WdgTexSelector + QWidget +
wdgtexselector.h
+ 1 +
+ + WdgTexView + QWidget +
wdgtexview.h
+ 1 +
WdgRigEditor QWidget
wdgrigeditor.h
1
+ + WdgFrameSelector + QWidget +
wdgframeselector.h
+ 1 +
diff --git a/animator/rigger.cpp b/animator/rigger.cpp index d3197ac..06e121d 100644 --- a/animator/rigger.cpp +++ b/animator/rigger.cpp @@ -10,14 +10,12 @@ */ #include "rigger.h" +#include "drawer.h" #include #include -#include #include -static constexpr float D2R = 3.14159265f / 180.0f; - Rigger rigger; /*****************************************************************************/ @@ -30,24 +28,29 @@ void Rigger::init() { rig.init(); rig.animationAdd("idle"); // a default animation with one frame, ready to edit + rig.load("lastrig.rig"); // reload the last session if present (keeps the default otherwise) - rigMode = RIG_MODE_JOINTS; + mode = RIG_MODE_JOINTS; + flags = FLAG_DISPLAY_JOINTS | FLAG_DISPLAY_BONES; + editAllFrames = true; rigView = { 0.0f, 32.0f, 0.0f }; + selectedTextureID = 0; deselect(); } void Rigger::terminate() { + rig.save("lastrig.rig"); // remember this session for the next launch rig.terminate(); } /*****************************************************************************/ void Rigger::selectAll() { - switch (rigMode) { + switch (mode) { case RIG_MODE_JOINTS: jointSelectAll(); break; - case RIG_MODE_BONES: boneSelectAll(); break; + case RIG_MODE_BONES: boneSelectAll(); break; default: break; } } @@ -55,7 +58,7 @@ void Rigger::selectAll() void Rigger::deselect() { selectedJoint = RIG_UNSELECTED; - selectedBone = RIG_UNSELECTED; + selectedBone = RIG_UNSELECTED; jointDeselectAll(); boneDeselectAll(); } @@ -85,7 +88,7 @@ void Rigger::jointSelect(int jId) cur->joints[jId].selected = true; selectedJoint = jId; - rigMode = RIG_MODE_JOINTS; + mode = RIG_MODE_JOINTS; } bool Rigger::jointAdd(QVector3D pos, int & jId) @@ -111,7 +114,7 @@ bool Rigger::jointAdd(QVector3D pos, int & jId) } selectedJoint = jId; - rigMode = RIG_MODE_JOINTS; + mode = RIG_MODE_JOINTS; return true; } @@ -138,8 +141,8 @@ void Rigger::jointDelete(int jId) if (b.jointID2 > jId) b.jointID2--; } - if (selectedJoint == jId) selectedJoint = RIG_UNSELECTED; - else if (selectedJoint > jId) selectedJoint--; + if (selectedJoint == jId) selectedJoint = RIG_UNSELECTED; + else if (selectedJoint > jId) selectedJoint--; } void Rigger::jointSelectAll() @@ -206,7 +209,7 @@ void Rigger::boneSelect(int bId) if (bId < 0 || bId >= rig.bones.count()) return; rig.bones[bId].selected = true; selectedBone = bId; - rigMode = RIG_MODE_BONES; + mode = RIG_MODE_BONES; } bool Rigger::boneAdd(int j1, int j2, int & bId) @@ -218,13 +221,14 @@ bool Rigger::boneAdd(int j1, int j2, int & bId) if (j2 < 0 || j2 >= cur->joints.count()) return false; Bone b{}; - b.jointID1 = (uint16_t) j1; - b.jointID2 = (uint16_t) j2; - b.width = 8.0f; - b.length = 0.0f; - b.offset = 0.0f; + b.jointID1 = (uint16_t) j1; + b.jointID2 = (uint16_t) j2; + b.width = 8.0f; + b.length = 0.0f; + b.offset = 0.0f; + b.minWidth = 1.0f; b.imageCount = 0; - b.selected = true; + b.selected = true; rig.bones.append(b); bId = rig.bones.count() - 1; @@ -321,4 +325,153 @@ QVector2D Rigger::to2D(const QVector3D & pos) const float a = rigView.pan * D2R; float x = pos.x() * cosf(a) + pos.z() * sinf(a); return QVector2D(x, -pos.y()); +} + +/*****************************************************************************/ +void Rigger::renderFlesh(QImage & image, const QVector2D & org, float zoom, const QList & joints) +{ + image.fill(Qt::transparent); + + QImage & src = rig.textures; + if (src.isNull()) return; + + QImage strip = src.format() == QImage::Format_ARGB32 ? src + : src.convertToFormat(QImage::Format_ARGB32); + int size = strip.width(); + if (size <= 0) return; + int count = strip.height() / size; + if (count <= 0) return; + + Texture texture; + texture.pixels = reinterpret_cast(strip.bits()); + texture.size = (uint16_t) size; + texture.count = (uint16_t) count; + texture.mask = (uint16_t) (size - 1); + texture.block = (uint32_t) (size * size); + + for (int i = 0; i < rig.bones.count(); i++) { + const Bone & b = rig.bones[i]; + if (b.flags & BONE_FLAG_INVISIBLE) continue; + if (b.imageCount == 0) continue; + if (b.jointID1 >= joints.count()) continue; + if (b.jointID2 >= joints.count()) continue; + + QVector2D p1 = org + to2D(joints[b.jointID1].pos) * zoom; + QVector2D p2 = org + to2D(joints[b.jointID2].pos) * zoom; + + QVector2D axis = p2 - p1; + float len = axis.length(); + QVector2D dir = len > 0.0001f ? axis / len : QVector2D(0.0f, -1.0f); + QVector2D perp = QVector2D(-dir.y(), dir.x()); + + QVector2D mid = (p1 + p2) * 0.5f + dir * (b.offset * zoom); + float halfLen = (len + b.length * zoom) * 0.5f; + float halfWid = (b.width * zoom) * 0.5f; + + // Rotate: the flat quad foreshortens with the view angle (edge-on at 90 deg). + // minWidth floors the magnitude so the texture never fully vanishes, + // while the sign of cos still flips the quad past the 90 deg mark. + if (b.flags & BONE_FLAG_ROTATE) { + float c = cosf(rigView.pan * D2R); + float w = halfWid * c; + float minHalf = (b.minWidth * zoom) * 0.5f; + if (fabsf(w) < minHalf) + w = copysignf(minHalf, c != 0.0f ? c : 1.0f); + halfWid = w; + } + + QVector2D c0 = mid - dir * halfLen - perp * halfWid; + QVector2D c1 = mid + dir * halfLen - perp * halfWid; + QVector2D c2 = mid + dir * halfLen + perp * halfWid; + QVector2D c3 = mid - dir * halfLen + perp * halfWid; + + // Pick the arc image facing the current view angle + float step = 360.0f / b.imageCount; + int arc = ((int) roundf(rigView.pan / step)) % b.imageCount; + if (arc < 0) arc += b.imageCount; + uint16_t surfaceId = b.images[arc]; + if (surfaceId >= count) continue; + + // Mirror: flip the texture across its width axis (the horizontal of the image) + float v0 = (b.flags & BONE_FLAG_MIRROR) ? 1.0f : 0.0f; + float v1 = (b.flags & BONE_FLAG_MIRROR) ? 0.0f : 1.0f; + + // Normalised UVs: u runs along the bone (length), v across it (width) + Quad q; + q.xs[0] = c0.x(); q.ys[0] = c0.y(); q.us[0] = 0.0f; q.vs[0] = v0; + q.xs[1] = c1.x(); q.ys[1] = c1.y(); q.us[1] = 1.0f; q.vs[1] = v0; + q.xs[2] = c2.x(); q.ys[2] = c2.y(); q.us[2] = 1.0f; q.vs[2] = v1; + q.xs[3] = c3.x(); q.ys[3] = c3.y(); q.us[3] = 0.0f; q.vs[3] = v1; + q.surfaceId = surfaceId; + + drawerQuad(image, &q, texture); + } +} + +/*****************************************************************************/ +void Rigger::renderAnimationFrame(QImage & image, int animationId, float frameCursor) +{ + if (animationId < 0 || animationId >= rig.animations.count()) { + image.fill(Qt::transparent); + return; + } + + Animation & a = rig.animations[animationId]; + int count = a.frames.count(); + if (count == 0) { + image.fill(Qt::transparent); + return; + } + +// Wrap the cursor into [0, count) and split into the two neighbour frames + float cursor = fmodf(frameCursor, (float) count); + if (cursor < 0.0f) cursor += (float) count; + int f0 = (int) floorf(cursor); + int f1 = (f0 + 1) % count; + float t = cursor - (float) f0; + + const Frame & frameA = a.frames[f0]; + const Frame & frameB = a.frames[f1]; + int jointCount = std::min(frameA.joints.count(), frameB.joints.count()); + +// Interpolated pose; flags/selection do not matter for rendering, only pos + QList pose; + pose.reserve(jointCount); + for (int i = 0; i < jointCount; i++) { + Joint j = frameA.joints[i]; + j.pos = frameA.joints[i].pos * (1.0f - t) + frameB.joints[i].pos * t; + pose.append(j); + } + +// Bounding box across every frame of the animation (same projection angle), +// so every baked frame of the sheet shares the same framing + QVector2D bbMin(1e9f, 1e9f), bbMax(-1e9f, -1e9f); + for (const Frame & f : a.frames) { + for (const Joint & j : f.joints) { + QVector2D p = to2D(j.pos); + bbMin.setX(std::min(bbMin.x(), p.x())); + bbMin.setY(std::min(bbMin.y(), p.y())); + bbMax.setX(std::max(bbMax.x(), p.x())); + bbMax.setY(std::max(bbMax.y(), p.y())); + } + } + +// Pad by the widest bone reach so the flesh quads are not clipped at the edges + float pad = 0.0f; + for (const Bone & b : rig.bones) + pad = std::max(pad, b.width * 0.5f + b.length * 0.5f + fabsf(b.offset)); + bbMin -= QVector2D(pad, pad); + bbMax += QVector2D(pad, pad); + + QVector2D bbSize = bbMax - bbMin; + if (bbSize.x() <= 0.0f || bbSize.y() <= 0.0f) { + image.fill(Qt::transparent); + return; + } + + float fitZoom = std::min(image.width() / bbSize.x(), image.height() / bbSize.y()); + QVector2D bbCenter = (bbMin + bbMax) * 0.5f; + QVector2D org = QVector2D(image.width() * 0.5f, image.height() * 0.5f) - bbCenter * fitZoom; + + renderFlesh(image, org, fitZoom, pose); } \ No newline at end of file diff --git a/animator/rigger.h b/animator/rigger.h index 52c993a..cd7ff32 100644 --- a/animator/rigger.h +++ b/animator/rigger.h @@ -14,11 +14,12 @@ #include "rigobjects.h" #include "rig.h" -//#include "renderer.h" +#include "primitives.h" #include #include #include +#include #include #include @@ -29,6 +30,13 @@ typedef enum { RIG_MODE_CONFIG, } RIG_MODES; +typedef enum : uint32_t { + FLAG_DISPLAY_JOINTS = 0x0001, + FLAG_DISPLAY_BONES = 0x0002, + FLAG_DISPLAY_FLESH = 0x0004, + FLAGS_DEFAULT = FLAG_DISPLAY_JOINTS | FLAG_DISPLAY_BONES | FLAG_DISPLAY_FLESH, +} RIG_FLAGS; + /*****************************************************************************/ constexpr int RIGGER_JOINT_RADIUS = 8; constexpr int RIGGER_BONE_RADIUS = 16; @@ -36,16 +44,6 @@ constexpr int RIGGER_BONE_RADIUS = 16; /// \brief Pixels spanned by the view diameter (sets the world-to-screen scale) constexpr float RIGGER_VIEW_SCALE = 256.0f; -/** - \brief Orthographic cylinder view: the world rotated by pan about Y, seen - from a Z distance (diameter), with a vertical offset -*/ -struct RigView { - float pan; ///< rotation about the Y axis, in degrees - float diameter; ///< cylinder diameter / camera Z distance - float y; ///< vertical offset -}; - /*****************************************************************************/ class Rigger { @@ -54,8 +52,11 @@ class Rigger void init(); void terminate(); - RIG_MODES rigMode; - RigView rigView; + RIG_MODES mode; + uint32_t flags; + + bool editAllFrames; + ViewpointOrtho rigView; /// \brief World-to-screen scale, derived from the view diameter float zoom() const { @@ -66,10 +67,7 @@ class Rigger int selectedJoint; int selectedBone; - - //bool inView(const QVector3D & pos) const { - // return pos.y() >= viewMinY && pos.y() <= viewMaxY; - //} + uint16_t selectedTextureID; void selectAll(); void deselect(); @@ -97,6 +95,27 @@ class Rigger /// \brief Project a world position onto the 2D cylinder view plane QVector2D to2D(const QVector3D & pos) const; + + /** + \brief Render the bone flesh of a single pose into an image + + The image is cleared to transparent first. \p org / \p zoom map world + units to image pixels, the same way the live editor view does; \p joints + is the pose to render (indexed the same way as the bones' jointID1/2). + */ + void renderFlesh(QImage & image, const QVector2D & org, float zoom, const QList & joints); + + /** + \brief Render an interpolated pose of an animation, fitted to its + bounding box, into an image (e.g. for sprite sheet baking) + + \param image destination, cleared to transparent and entirely filled + by the animation's (padded) bounding box + \param animationId index into rig.animations + \param frameCursor frame position; interpolates between the floor and + ceiling neighbour frames, wrapping across the animation's loop + */ + void renderAnimationFrame(QImage & image, int animationId, float frameCursor); }; extern Rigger rigger; diff --git a/animator/rigger.pro b/animator/rigger.pro index b183990..9e61e90 100644 --- a/animator/rigger.pro +++ b/animator/rigger.pro @@ -23,15 +23,24 @@ SOURCES += \ mainwindow.cpp \ rigger.cpp \ wdgrigeditor.cpp \ + wdgtexselector.cpp \ + wdgframeselector.cpp \ + wdgtexview.cpp \ ../common/engine/rig.cpp \ - ../common/engine/rig_io.cpp + ../common/engine/rig_io.cpp \ + ../common/engine/drawer.cpp HEADERS += \ mainwindow.h \ rigger.h \ wdgrigeditor.h \ + wdgtexselector.h \ + wdgframeselector.h \ + wdgtexview.h \ ../common/engine/rig.h \ - ../common/engine/rigobjects.h + ../common/engine/rigobjects.h \ + ../common/engine/drawer.h \ + ../common/engine/primitives.h FORMS += \ mainwindow.ui diff --git a/animator/wdgframeselector.cpp b/animator/wdgframeselector.cpp new file mode 100644 index 0000000..1d21ec3 --- /dev/null +++ b/animator/wdgframeselector.cpp @@ -0,0 +1,109 @@ +/** + RIGGER + Animation editor for the DIE engine + (c) Fred's Lab 2024-2026 + Frédéric Meslin / info@fredslab.net + SPDX-License-Identifier: MIT + If used commercially, contributions, donations are highly appreciated. + + animation frame selector widget +*/ + +#include "wdgframeselector.h" + +#include "rigger.h" +#include "mainwindow.h" + +#include +#include + +/*****************************************************************************/ +WdgFrameSelector::WdgFrameSelector(QWidget * parent) : + QWidget(parent), + scroll(0) +{ +} + +/*****************************************************************************/ +void WdgFrameSelector::paintEvent(QPaintEvent *) +{ + QPainter painter(this); + painter.fillRect(rect(), Qt::black); + + Animation * anim = rigger.rig.currentAnimationPtr(); + if (!anim) return; + + int count = anim->frames.count(); + int cardWidth = height(); + if (count <= 0 || cardWidth <= 0) return; + + int cardSpace = width() / cardWidth; + int overflow = count * cardWidth - width(); + if (overflow < 0) overflow = 0; + int offset = (overflow * scroll) / 1000; + + int start = offset / cardWidth; + int stop = start + cardSpace + 2; + if (stop > count) stop = count; + int shift = offset % cardWidth; + + QFont font = painter.font(); + font.setBold(true); + painter.setFont(font); + + for (int i = start; i < stop; i++) { + int x = (i - start) * cardWidth - shift; + QRect card(x, 0, cardWidth, cardWidth); + + bool isCurrent = (i == rigger.rig.currentFrame); + painter.setPen(QPen(Qt::white)); + painter.setBrush(isCurrent ? QColor(96, 96, 96) : QColor(48, 48, 48)); + painter.drawRect(card.adjusted(1, 1, -2, -2)); + + painter.setPen(isCurrent ? Qt::white : QColor(160, 160, 160)); + painter.drawText(card, Qt::AlignCenter, QString::number(i)); + } +} + +/*****************************************************************************/ +void WdgFrameSelector::mousePressEvent(QMouseEvent * event) +{ + Animation * anim = rigger.rig.currentAnimationPtr(); + if (!anim) return; + + int count = anim->frames.count(); + int cardWidth = height(); + if (count <= 0 || cardWidth <= 0) return; + + int overflow = count * cardWidth - width(); + if (overflow < 0) overflow = 0; + int offset = (overflow * scroll) / 1000; + + int fId = (event->position().x() + offset) / cardWidth; + if (fId < 0 || fId >= count) return; + + rigger.rig.frameSelect(fId); + if (mainWindow) { + mainWindow->updateJointProperties(); + mainWindow->updateRigCanvas(); + } + update(); +} + +/*****************************************************************************/ +void WdgFrameSelector::setScroll(int scroll) +{ + this->scroll = scroll; + update(); +} + +/*****************************************************************************/ +void WdgFrameSelector::wheelEvent(QWheelEvent * event) +{ + QPoint degrees = event->angleDelta(); + if (degrees.y() > 0) scroll -= 25; + if (degrees.y() < 0) scroll += 25; + scroll = qBound(0, scroll, 999); + event->accept(); + update(); +} diff --git a/animator/wdgframeselector.h b/animator/wdgframeselector.h new file mode 100644 index 0000000..720ef62 --- /dev/null +++ b/animator/wdgframeselector.h @@ -0,0 +1,36 @@ +/** + RIGGER + Animation editor for the DIE engine + (c) Fred's Lab 2024-2026 + Frédéric Meslin / info@fredslab.net + SPDX-License-Identifier: MIT + If used commercially, contributions, donations are highly appreciated. + + animation frame selector widget +*/ + +#ifndef WDG_FRAMESELECTOR_H +#define WDG_FRAMESELECTOR_H + +#include + +class WdgFrameSelector : public QWidget +{ + Q_OBJECT + +public: + explicit WdgFrameSelector(QWidget * parent = nullptr); + + void setScroll(int scroll); + int getScroll() { return scroll; } + +private: + int scroll; + +protected: + void paintEvent(QPaintEvent * event) override; + void mousePressEvent(QMouseEvent * event) override; + void wheelEvent(QWheelEvent * event) override; +}; + +#endif // WDG_FRAMESELECTOR_H diff --git a/animator/wdgrigeditor.cpp b/animator/wdgrigeditor.cpp index 3e6e5a4..42c79ca 100644 --- a/animator/wdgrigeditor.cpp +++ b/animator/wdgrigeditor.cpp @@ -13,14 +13,13 @@ #include "rigger.h" #include "mainwindow.h" +#include "primitives.h" // D2R #include #include #include -static constexpr float D2R = 3.14159265f / 180.0f; - /*****************************************************************************/ QPen WdgRigEditor::colorPrincipal = QColor(255, 255, 255); QPen WdgRigEditor::colorSelected = QColor(192, 192, 192); @@ -60,6 +59,17 @@ void WdgRigEditor::drawAxes(QPainter & painter, const QVector2D & org) painter.drawLine(QPointF(0, org.y()), QPointF(width(), org.y())); // ground } +void WdgRigEditor::drawFlesh(QPainter & painter, const QVector2D & org) +{ + Frame * cur = rigger.rig.currentFramePtr(); + if (!cur) return; + +// The textured quads are rasterised into their own ARGB layer, then blitted + QImage flesh(width(), height(), QImage::Format_ARGB32); + rigger.renderFlesh(flesh, org, rigger.zoom(), cur->joints); + painter.drawImage(0, 0, flesh); +} + void WdgRigEditor::drawBones(QPainter & painter, const QVector2D & org) { Frame * cur = rigger.rig.currentFramePtr(); @@ -75,12 +85,41 @@ void WdgRigEditor::drawBones(QPainter & painter, const QVector2D & org) QVector2D p1 = org + rigger.to2D(cur->joints[b.jointID1].pos) * zoom; QVector2D p2 = org + rigger.to2D(cur->joints[b.jointID2].pos) * zoom; - if (i == rigger.selectedBone) painter.setPen(colorPrincipal); - else if (b.selected) painter.setPen(colorSelected); - else painter.setPen(colorBoneBase); - + // Bone axis and its perpendicular (default to vertical when degenerate) + QVector2D axis = p2 - p1; + float len = axis.length(); + QVector2D dir = len > 0.0001f ? axis / len : QVector2D(0.0f, -1.0f); + QVector2D perp = QVector2D(-dir.y(), dir.x()); + + // Quad = joint span + extra length, shifted by offset, spread by width + QVector2D mid = (p1 + p2) * 0.5f + dir * (b.offset * zoom); + float halfLen = (len + b.length * zoom) * 0.5f; + float halfWid = (b.width * zoom) * 0.5f; + + QPointF quad[4] = { + (mid - dir * halfLen - perp * halfWid).toPointF(), + (mid + dir * halfLen - perp * halfWid).toPointF(), + (mid + dir * halfLen + perp * halfWid).toPointF(), + (mid - dir * halfLen + perp * halfWid).toPointF(), + }; + + QColor color = colorBoneBase.color(); + if (i == rigger.selectedBone) color = colorPrincipal.color(); + else if (b.selected) color = colorSelected.color(); + + QColor fill = color; + fill.setAlpha(48); + + // Billboard quad: translucent fill + outline + painter.setPen(color); + painter.setBrush(fill); + painter.drawPolygon(quad, 4); + + // Bone axis (joint-to-joint) + painter.setBrush(Qt::NoBrush); painter.drawLine(p1.toPointF(), p2.toPointF()); } + painter.setBrush(Qt::NoBrush); painter.setRenderHint(QPainter::Antialiasing, false); } @@ -121,8 +160,9 @@ void WdgRigEditor::paintEvent(QPaintEvent *) QVector2D org = origin(); drawAxes(painter, org); - drawBones(painter, org); - drawJoints(painter, org); + if (rigger.flags & FLAG_DISPLAY_FLESH) drawFlesh(painter, org); + if (rigger.flags & FLAG_DISPLAY_BONES) drawBones(painter, org); + if (rigger.flags & FLAG_DISPLAY_JOINTS) drawJoints(painter, org); if (selectRegion) { painter.setPen(Qt::white); @@ -150,29 +190,68 @@ void WdgRigEditor::mousePressEvent(QMouseEvent * event) selectRegionStart = true; selectRegionC1 = selectRegionC2 = click; - if (rigger.rigMode != RIG_MODE_JOINTS) return; - bool shift = event->modifiers() & Qt::ShiftModifier; QVector2D world = getWorldCoordinates(click); pressWorld = world; - int jId; - if (rigger.jointFindInCircle(world, jId)) { - // Hit a joint: select it (keep the group when shift / already selected) - Frame * cur = rigger.rig.currentFramePtr(); - bool already = cur && jId < cur->joints.count() && cur->joints[jId].selected; - if (!shift && !already) rigger.jointDeselectAll(); - selectRegionStart = false; - rigger.selectedJoint = jId; - if (cur && jId < cur->joints.count()) cur->joints[jId].selected = true; - - } else { - // Missed: clear the selection, a region or a create may follow - rigger.selectedJoint = RIG_UNSELECTED; - rigger.jointDeselectAll(); + if (rigger.mode == RIG_MODE_JOINTS) { + int jId; + if (rigger.jointFindInCircle(world, jId)) { + // Hit a joint: select it (keep the group when shift / already selected) + Frame * cur = rigger.rig.currentFramePtr(); + bool already = cur && jId < cur->joints.count() && cur->joints[jId].selected; + if (!shift && !already) rigger.jointDeselectAll(); + selectRegionStart = false; + rigger.selectedJoint = jId; + if (cur && jId < cur->joints.count()) cur->joints[jId].selected = true; + + } else { + // Missed: clear the selection, a region or a create may follow + rigger.selectedJoint = RIG_UNSELECTED; + rigger.jointDeselectAll(); + } + + } else if (rigger.mode == RIG_MODE_BONES) { + int jId; + if (rigger.jointFindInCircle(world, jId)) { + // Clicking joints chains bones: anchor joint -> clicked joint + selectRegionStart = false; + if (rigger.selectedJoint != RIG_UNSELECTED && rigger.selectedJoint != jId) { + int bId; + if (rigger.boneAdd(rigger.selectedJoint, jId, bId)) { + rigger.boneDeselectAll(); + rigger.boneSelect(bId); + } + } + rigger.jointDeselectAll(); + rigger.selectedJoint = jId; // becomes the next anchor + Frame * cur = rigger.rig.currentFramePtr(); + if (cur && jId < cur->joints.count()) cur->joints[jId].selected = true; + + } else { + int bId; + if (rigger.boneFindInCircle(world, bId)) { + // Hit a bone: select it + selectRegionStart = false; + if (!shift) rigger.boneDeselectAll(); + rigger.boneSelect(bId); + rigger.jointDeselectAll(); + rigger.selectedJoint = RIG_UNSELECTED; + + } else { + // Missed: clear, a region may follow + rigger.boneDeselectAll(); + rigger.selectedBone = RIG_UNSELECTED; + rigger.jointDeselectAll(); + rigger.selectedJoint = RIG_UNSELECTED; + } + } } - if (mainWindow) mainWindow->updateJointProperties(); + if (mainWindow) { + mainWindow->updateJointProperties(); + mainWindow->updateBoneProperties(); + } update(); } @@ -192,11 +271,10 @@ void WdgRigEditor::mouseMoveEvent(QMouseEvent * event) } if (!(event->buttons() & Qt::LeftButton)) return; - if (rigger.rigMode != RIG_MODE_JOINTS) return; QVector2D click(event->position()); -// Press landed on empty space: grow a selection rectangle +// Press landed on empty space: grow a selection rectangle (joints or bones) if (selectRegionStart) { selectRegion = true; selectRegionC2 = click; @@ -204,6 +282,9 @@ void WdgRigEditor::mouseMoveEvent(QMouseEvent * event) return; } +// Dragging joints only happens in joint mode + if (rigger.mode != RIG_MODE_JOINTS) return; + // Press landed on a joint: drag the selection within the current viewing // plane, preserving each joint's depth so it doesn't snap to the axis plane Frame * cur = rigger.rig.currentFramePtr(); @@ -219,12 +300,23 @@ void WdgRigEditor::mouseMoveEvent(QMouseEvent * event) float vx = world.x(); QVector3D target(vx * ca - depth * sa, -world.y(), vx * sa + depth * ca); -// Translate every selected joint by the primary's delta (rigid group move) +// Translate every selected joint by the primary's delta (rigid group move). +// With editAllFrames the same joint index is shifted across every frame. QVector3D delta = target - jp.pos; - for (Joint & j : cur->joints) { - if (!j.selected) continue; - j.pos += delta; - j.apos = j.pos; + for (int idx = 0; idx < cur->joints.count(); idx++) { + if (!cur->joints[idx].selected) continue; + + if (rigger.editAllFrames) { + for (Animation & a : rigger.rig.animations) + for (Frame & f : a.frames) { + if (idx >= f.joints.count()) continue; + f.joints[idx].pos += delta; + f.joints[idx].apos = f.joints[idx].pos; + } + } else { + cur->joints[idx].pos += delta; + cur->joints[idx].apos = cur->joints[idx].pos; + } } if (mainWindow) mainWindow->updateJointProperties(); @@ -233,7 +325,9 @@ void WdgRigEditor::mouseMoveEvent(QMouseEvent * event) void WdgRigEditor::mouseReleaseEvent(QMouseEvent * event) { - if (mouseLeftWasPressed && rigger.rigMode == RIG_MODE_JOINTS) { + bool shift = event->modifiers() & Qt::ShiftModifier; + + if (mouseLeftWasPressed && rigger.mode == RIG_MODE_JOINTS) { if (!selectRegion) { // A plain click on empty space drops a new joint on the camera plane if (rigger.selectedJoint == RIG_UNSELECTED) { @@ -246,7 +340,6 @@ void WdgRigEditor::mouseReleaseEvent(QMouseEvent * event) } else { // A dragged rectangle selects every joint inside it - bool shift = event->modifiers() & Qt::ShiftModifier; if (!shift) rigger.jointDeselectAll(); QVector2D c1 = getWorldCoordinates(selectRegionC1); QVector2D c2 = getWorldCoordinates(selectRegionC2); @@ -255,7 +348,24 @@ void WdgRigEditor::mouseReleaseEvent(QMouseEvent * event) rigger.selectedJoint = jId; } - if (mainWindow) mainWindow->updateJointProperties(); + } else if (mouseLeftWasPressed && rigger.mode == RIG_MODE_BONES) { + if (selectRegion) { + // A dragged rectangle selects every bone inside it + if (!shift) rigger.boneDeselectAll(); + QVector2D c1 = getWorldCoordinates(selectRegionC1); + QVector2D c2 = getWorldCoordinates(selectRegionC2); + int bId; + if (rigger.boneFindInRect(c1, c2, bId)) + rigger.selectedBone = bId; + rigger.jointDeselectAll(); + rigger.selectedJoint = RIG_UNSELECTED; + } + // a plain click was already resolved on press + } + + if (mouseLeftWasPressed && mainWindow) { + mainWindow->updateJointProperties(); + mainWindow->updateBoneProperties(); } selectRegion = false; diff --git a/animator/wdgrigeditor.h b/animator/wdgrigeditor.h index 8428c83..6afac35 100644 --- a/animator/wdgrigeditor.h +++ b/animator/wdgrigeditor.h @@ -51,6 +51,7 @@ class WdgRigEditor : public QWidget QVector2D getWorldCoordinates(const QVector2D & screen) const; void drawAxes(QPainter & painter, const QVector2D & org); + void drawFlesh(QPainter & painter, const QVector2D & org); void drawBones(QPainter & painter, const QVector2D & org); void drawJoints(QPainter & painter, const QVector2D & org); }; diff --git a/animator/wdgtexselector.cpp b/animator/wdgtexselector.cpp new file mode 100644 index 0000000..fc0bd8a --- /dev/null +++ b/animator/wdgtexselector.cpp @@ -0,0 +1,107 @@ +/** + RIGGER + Animation editor for the DIE engine + (c) Fred's Lab 2024-2026 + Frédéric Meslin / info@fredslab.net + SPDX-License-Identifier: MIT + If used commercially, contributions, donations are highly appreciated. + + texture strip selector widget +*/ + +#include "wdgtexselector.h" + +#include "rigger.h" +#include "mainwindow.h" + +#include +#include + +#include + +/*****************************************************************************/ +WdgTexSelector::WdgTexSelector(QWidget * parent) : + QWidget(parent), + scroll(0) +{ +} + +/*****************************************************************************/ +void WdgTexSelector::paintEvent(QPaintEvent *) +{ + QPainter painter(this); + painter.fillRect(rect(), Qt::black); + painter.setPen(QPen(Qt::black)); + + QImage & strip = rigger.rig.textures; + int texWidth = strip.width(); + int tileWidth = height(); + if (texWidth <= 0 || tileWidth <= 0) return; + + int texCount = strip.height() / texWidth; + int tileSpace = width() / tileWidth; + + int overflow = texCount * tileWidth - width(); + if (overflow < 0) overflow = 0; + int offset = (overflow * scroll) / 1000; + + int start = offset / tileWidth; + int stop = start + tileSpace + 2; + if (stop > texCount) stop = texCount; + int shift = offset % tileWidth; + + int cursor = 0; + for (int i = start; i < stop; i++) { + QRect source = QRect(0, i * texWidth, texWidth, texWidth); + QRect target = QRect(cursor++ * tileWidth, 0, tileWidth, tileWidth); + painter.drawImage(target, strip, source); + } + + QBrush brush = QBrush(QColor(255, 255, 255, 128)); + painter.setPen(Qt::NoPen); + painter.setBrush(brush); + + int x = rigger.selectedTextureID * tileWidth - offset; + QRect selection = QRect(x, 0, tileWidth, tileWidth); + painter.drawRect(selection); +} + +/*****************************************************************************/ +void WdgTexSelector::mousePressEvent(QMouseEvent * event) +{ + QImage & strip = rigger.rig.textures; + + int texWidth = strip.width(); + int tileWidth = height(); + if (texWidth <= 0 || tileWidth <= 0) return; + + int texCount = strip.height() / texWidth; + int overflow = texCount * tileWidth - width(); + if (overflow < 0) overflow = 0; + int offset = (overflow * scroll) / 1000; + + int texId = (event->position().x() + offset) / tileWidth; + if (texId < 0 || texId >= texCount) return; + + rigger.selectedTextureID = (uint16_t) texId; + if (mainWindow) mainWindow->setTexture((uint16_t) texId); + update(); +} + +/*****************************************************************************/ +void WdgTexSelector::setScroll(int scroll) +{ + this->scroll = scroll; + update(); +} + +/*****************************************************************************/ +void WdgTexSelector::wheelEvent(QWheelEvent * event) +{ + QPoint degrees = event->angleDelta(); + if (degrees.y() > 0) scroll -= 25; + if (degrees.y() < 0) scroll += 25; + scroll = qBound(0, scroll, 999); + event->accept(); + update(); +} diff --git a/animator/wdgtexselector.h b/animator/wdgtexselector.h new file mode 100644 index 0000000..4c264dd --- /dev/null +++ b/animator/wdgtexselector.h @@ -0,0 +1,36 @@ +/** + RIGGER + Animation editor for the DIE engine + (c) Fred's Lab 2024-2026 + Frédéric Meslin / info@fredslab.net + SPDX-License-Identifier: MIT + If used commercially, contributions, donations are highly appreciated. + + texture strip selector widget +*/ + +#ifndef WDG_TEX_SELECTOR_H +#define WDG_TEX_SELECTOR_H + +#include + +class WdgTexSelector : public QWidget +{ + Q_OBJECT + +public: + explicit WdgTexSelector(QWidget * parent = nullptr); + + void setScroll(int scroll); + int getScroll() { return scroll; } + +private: + int scroll; + +protected: + void paintEvent(QPaintEvent * event) override; + void mousePressEvent(QMouseEvent * event) override; + void wheelEvent(QWheelEvent * event) override; +}; + +#endif // WDG_TEX_SELECTOR_H diff --git a/animator/wdgtexview.cpp b/animator/wdgtexview.cpp new file mode 100644 index 0000000..33307a3 --- /dev/null +++ b/animator/wdgtexview.cpp @@ -0,0 +1,45 @@ +/** + RIGGER + Animation editor for the DIE engine + (c) Fred's Lab 2024-2026 + Frédéric Meslin / info@fredslab.net + SPDX-License-Identifier: MIT + If used commercially, contributions, donations are highly appreciated. + + texture preview widget +*/ + +#include "wdgtexview.h" + +#include "rigger.h" + +#include + +/*****************************************************************************/ +WdgTexView::WdgTexView(QWidget * parent) : + QWidget(parent), + id(0) +{ +} + +/*****************************************************************************/ +void WdgTexView::setID(uint16_t id) +{ + this->id = id; + update(); +} + +/*****************************************************************************/ +void WdgTexView::paintEvent(QPaintEvent *) +{ + QPainter painter(this); + painter.fillRect(rect(), Qt::black); + + QImage & strip = rigger.rig.textures; + int size = strip.width(); + if (size <= 0) return; + + painter.setRenderHint(QPainter::Antialiasing); + QRect source = QRect(0, id * size, size, size); + painter.drawImage(rect(), strip, source); +} diff --git a/animator/wdgtexview.h b/animator/wdgtexview.h new file mode 100644 index 0000000..dc79630 --- /dev/null +++ b/animator/wdgtexview.h @@ -0,0 +1,34 @@ +/** + RIGGER + Animation editor for the DIE engine + (c) Fred's Lab 2024-2026 + Frédéric Meslin / info@fredslab.net + SPDX-License-Identifier: MIT + If used commercially, contributions, donations are highly appreciated. + + texture preview widget +*/ + +#ifndef WDG_TEXVIEW_H +#define WDG_TEXVIEW_H + +#include +#include + +class WdgTexView : public QWidget +{ + Q_OBJECT + +public: + explicit WdgTexView(QWidget * parent = nullptr); + + void setID(uint16_t id); + +private: + uint16_t id; + +protected: + void paintEvent(QPaintEvent * event) override; +}; + +#endif // WDG_TEXVIEW_H diff --git a/common/engine/rig_io.cpp b/common/engine/rig_io.cpp index 4f1a8e0..c3abb12 100644 --- a/common/engine/rig_io.cpp +++ b/common/engine/rig_io.cpp @@ -43,11 +43,11 @@ bool Rig::save(const QString & filename) // ==== BONES ==== fprintf(file, "# == BONES ==\n"); - fprintf(file, "# format: B jointID1, jointID2, width, length, offset, imageCount, flags\n"); + fprintf(file, "# format: B jointID1, jointID2, width, length, offset, minWidth, imageCount, flags\n"); fprintf(file, "# format: I index, imageID\n"); for (const Bone & b : bones) { - fprintf(file, "B %04hu, %04hu, %+4.4f, %+4.4f, %+4.4f, %02hu, %04hx\n", - b.jointID1, b.jointID2, b.width, b.length, b.offset, b.imageCount, b.flags); + fprintf(file, "B %04hu, %04hu, %+4.4f, %+4.4f, %+4.4f, %+4.4f, %02hu, %04hx\n", + b.jointID1, b.jointID2, b.width, b.length, b.offset, b.minWidth, b.imageCount, b.flags); uint16_t count = b.imageCount > RIG_BONE_IMAGES_MAX ? RIG_BONE_IMAGES_MAX : b.imageCount; for (int i = 0; i < count; i++) fprintf(file, "\tI %02d, %04hu\n", i, b.images[i]); @@ -120,8 +120,8 @@ bool Rig::load(const QString & filename) }else if (c == 'B') { Bone b{}; - fscanf(file, "%hu, %hu, %f, %f, %f, %hu, %hx\n", - &b.jointID1, &b.jointID2, &b.width, &b.length, &b.offset, &b.imageCount, &b.flags); + fscanf(file, "%hu, %hu, %f, %f, %f, %f, %hu, %hx\n", + &b.jointID1, &b.jointID2, &b.width, &b.length, &b.offset, &b.minWidth, &b.imageCount, &b.flags); if (b.imageCount > RIG_BONE_IMAGES_MAX) b.imageCount = RIG_BONE_IMAGES_MAX; bones.append(b); diff --git a/common/engine/rigobjects.h b/common/engine/rigobjects.h index acd515b..63f72cc 100644 --- a/common/engine/rigobjects.h +++ b/common/engine/rigobjects.h @@ -15,7 +15,7 @@ #include #include -static constexpr int RIG_BONE_IMAGES_MAX = 8; +static constexpr int RIG_BONE_IMAGES_MAX = 4; ///< Front, Right, Back, Left static constexpr int RIG_UNSELECTED = -1; /*****************************************************************************/ @@ -40,6 +40,7 @@ typedef enum : uint16_t { BONE_FLAG_FREE = 0x0000, BONE_FLAG_INVISIBLE = 0x0001, BONE_FLAG_MIRROR = 0x0002, ///< flip the image horizontally (left / right reuse) + BONE_FLAG_ROTATE = 0x0004, ///< flesh width foreshortens with the view angle } BONE_FLAGS; /** @@ -52,9 +53,10 @@ typedef struct { float width; ///< quad width across the bone, in world units float length; ///< extra quad length added to the joint span, in world units float offset; ///< quad shift along the bone axis, in world units + float minWidth; ///< floor on the rendered width when BONE_FLAG_ROTATE foreshortens it - uint16_t images[RIG_BONE_IMAGES_MAX]; ///< per-arc image ids, [0] centred on the front - uint16_t imageCount; ///< number of arcs (1, 2, 4, 8...) + uint16_t images[RIG_BONE_IMAGES_MAX]; ///< per-arc image ids: 0 Front, 1 Right, 2 Back, 3 Left + uint16_t imageCount; ///< number of arcs in use, from the front (0..4) uint16_t flags; bool selected;