2025-12-01
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
GNU LESSER GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
|
||||
This version of the GNU Lesser General Public License incorporates
|
||||
the terms and conditions of version 3 of the GNU General Public
|
||||
License, supplemented by the additional permissions listed below.
|
||||
|
||||
0. Additional Definitions.
|
||||
|
||||
As used herein, "this License" refers to version 3 of the GNU Lesser
|
||||
General Public License, and the "GNU GPL" refers to version 3 of the GNU
|
||||
General Public License.
|
||||
|
||||
"The Library" refers to a covered work governed by this License,
|
||||
other than an Application or a Combined Work as defined below.
|
||||
|
||||
An "Application" is any work that makes use of an interface provided
|
||||
by the Library, but which is not otherwise based on the Library.
|
||||
Defining a subclass of a class defined by the Library is deemed a mode
|
||||
of using an interface provided by the Library.
|
||||
|
||||
A "Combined Work" is a work produced by combining or linking an
|
||||
Application with the Library. The particular version of the Library
|
||||
with which the Combined Work was made is also called the "Linked
|
||||
Version".
|
||||
|
||||
The "Minimal Corresponding Source" for a Combined Work means the
|
||||
Corresponding Source for the Combined Work, excluding any source code
|
||||
for portions of the Combined Work that, considered in isolation, are
|
||||
based on the Application, and not on the Linked Version.
|
||||
|
||||
The "Corresponding Application Code" for a Combined Work means the
|
||||
object code and/or source code for the Application, including any data
|
||||
and utility programs needed for reproducing the Combined Work from the
|
||||
Application, but excluding the System Libraries of the Combined Work.
|
||||
|
||||
1. Exception to Section 3 of the GNU GPL.
|
||||
|
||||
You may convey a covered work under sections 3 and 4 of this License
|
||||
without being bound by section 3 of the GNU GPL.
|
||||
|
||||
2. Conveying Modified Versions.
|
||||
|
||||
If you modify a copy of the Library, and, in your modifications, a
|
||||
facility refers to a function or data to be supplied by an Application
|
||||
that uses the facility (other than as an argument passed when the
|
||||
facility is invoked), then you may convey a copy of the modified
|
||||
version:
|
||||
|
||||
a) under this License, provided that you make a good faith effort to
|
||||
ensure that, in the event an Application does not supply the
|
||||
function or data, the facility still operates, and performs
|
||||
whatever part of its purpose remains meaningful, or
|
||||
|
||||
b) under the GNU GPL, with none of the additional permissions of
|
||||
this License applicable to that copy.
|
||||
|
||||
3. Object Code Incorporating Material from Library Header Files.
|
||||
|
||||
The object code form of an Application may incorporate material from
|
||||
a header file that is part of the Library. You may convey such object
|
||||
code under terms of your choice, provided that, if the incorporated
|
||||
material is not limited to numerical parameters, data structure
|
||||
layouts and accessors, or small macros, inline functions and templates
|
||||
(ten or fewer lines in length), you do both of the following:
|
||||
|
||||
a) Give prominent notice with each copy of the object code that the
|
||||
Library is used in it and that the Library and its use are
|
||||
covered by this License.
|
||||
|
||||
b) Accompany the object code with a copy of the GNU GPL and this license
|
||||
document.
|
||||
|
||||
4. Combined Works.
|
||||
|
||||
You may convey a Combined Work under terms of your choice that,
|
||||
taken together, effectively do not restrict modification of the
|
||||
portions of the Library contained in the Combined Work and reverse
|
||||
engineering for debugging such modifications, if you also do each of
|
||||
the following:
|
||||
|
||||
a) Give prominent notice with each copy of the Combined Work that
|
||||
the Library is used in it and that the Library and its use are
|
||||
covered by this License.
|
||||
|
||||
b) Accompany the Combined Work with a copy of the GNU GPL and this license
|
||||
document.
|
||||
|
||||
c) For a Combined Work that displays copyright notices during
|
||||
execution, include the copyright notice for the Library among
|
||||
these notices, as well as a reference directing the user to the
|
||||
copies of the GNU GPL and this license document.
|
||||
|
||||
d) Do one of the following:
|
||||
|
||||
0) Convey the Minimal Corresponding Source under the terms of this
|
||||
License, and the Corresponding Application Code in a form
|
||||
suitable for, and under terms that permit, the user to
|
||||
recombine or relink the Application with a modified version of
|
||||
the Linked Version to produce a modified Combined Work, in the
|
||||
manner specified by section 6 of the GNU GPL for conveying
|
||||
Corresponding Source.
|
||||
|
||||
1) Use a suitable shared library mechanism for linking with the
|
||||
Library. A suitable mechanism is one that (a) uses at run time
|
||||
a copy of the Library already present on the user's computer
|
||||
system, and (b) will operate properly with a modified version
|
||||
of the Library that is interface-compatible with the Linked
|
||||
Version.
|
||||
|
||||
e) Provide Installation Information, but only if you would otherwise
|
||||
be required to provide such information under section 6 of the
|
||||
GNU GPL, and only to the extent that such information is
|
||||
necessary to install and execute a modified version of the
|
||||
Combined Work produced by recombining or relinking the
|
||||
Application with a modified version of the Linked Version. (If
|
||||
you use option 4d0, the Installation Information must accompany
|
||||
the Minimal Corresponding Source and Corresponding Application
|
||||
Code. If you use option 4d1, you must provide the Installation
|
||||
Information in the manner specified by section 6 of the GNU GPL
|
||||
for conveying Corresponding Source.)
|
||||
|
||||
5. Combined Libraries.
|
||||
|
||||
You may place library facilities that are a work based on the
|
||||
Library side by side in a single library together with other library
|
||||
facilities that are not Applications and are not covered by this
|
||||
License, and convey such a combined library under terms of your
|
||||
choice, if you do both of the following:
|
||||
|
||||
a) Accompany the combined library with a copy of the same work based
|
||||
on the Library, uncombined with any other library facilities,
|
||||
conveyed under the terms of this License.
|
||||
|
||||
b) Give prominent notice with the combined library that part of it
|
||||
is a work based on the Library, and explaining where to find the
|
||||
accompanying uncombined form of the same work.
|
||||
|
||||
6. Revised Versions of the GNU Lesser General Public License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions
|
||||
of the GNU Lesser General Public License from time to time. Such new
|
||||
versions will be similar in spirit to the present version, but may
|
||||
differ in detail to address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Library as you received it specifies that a certain numbered version
|
||||
of the GNU Lesser General Public License "or any later version"
|
||||
applies to it, you have the option of following the terms and
|
||||
conditions either of that published version or of any later version
|
||||
published by the Free Software Foundation. If the Library as you
|
||||
received it does not specify a version number of the GNU Lesser
|
||||
General Public License, you may choose any version of the GNU Lesser
|
||||
General Public License ever published by the Free Software Foundation.
|
||||
|
||||
If the Library as you received it specifies that a proxy can decide
|
||||
whether future versions of the GNU Lesser General Public License shall
|
||||
apply, that proxy's public statement of acceptance of any version is
|
||||
permanent authorization for you to choose that version for the
|
||||
Library.
|
||||
@@ -0,0 +1,311 @@
|
||||
<div align="center">
|
||||
|
||||
# Rokoko Studio Live Plugin for Blender
|
||||
|
||||
[](https://www.blender.org/)
|
||||
[](https://www.rokoko.com/en/products/studio)
|
||||
[](LICENSE)
|
||||
|
||||
**Stream motion capture data from Rokoko Studio directly into Blender in real-time**
|
||||
|
||||
[📥 Download Latest](https://github.com/Rokoko/rokoko-studio-live-blender/archive/refs/heads/master.zip) •
|
||||
[📖 Documentation](https://support.rokoko.com/hc/en-us/categories/4410420388113-Rokoko-Plugins) •
|
||||
[💬 Support](https://support.rokoko.com/)
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Overview
|
||||
|
||||
[Rokoko Studio](https://www.rokoko.com/en/products/studio) is a powerful and intuitive software for recording, visualizing and exporting motion capture data. This official Blender plugin enables seamless real-time streaming of animation data from Rokoko Studio directly into Blender.
|
||||
|
||||
**Key Benefits:**
|
||||
- 🎭 **Real-time streaming** of motion capture data
|
||||
- 🎮 **Remote control** of Rokoko Studio from Blender
|
||||
- 🔄 **Easy retargeting** between different character rigs
|
||||
- 👥 **Multi-actor support** for complex scenes
|
||||
|
||||
## 📖 Table of Contents
|
||||
|
||||
- [🎯 Overview](#-overview)
|
||||
- [✨ Features](#-features)
|
||||
- [📋 Requirements](#-requirements)
|
||||
- [📦 Installation](#-installation)
|
||||
- [🎬 Getting Started for Streaming](#-getting-started-for-streaming)
|
||||
- [1. Prepare Your Model](#1-prepare-your-model)
|
||||
- [2. Enable Rokoko Studio Live](#2-enable-rokoko-studio-live)
|
||||
- [3. Receive Data in Blender](#3-receive-data-in-blender)
|
||||
- [4. Stream Character Data](#4-stream-character-data)
|
||||
- [5. Stream Face and Prop Data](#5-stream-face-and-prop-data)
|
||||
- [🎯 Retargeting](#-retargeting)
|
||||
- [💡 Tips & Troubleshooting](#-tips--troubleshooting)
|
||||
- [🤝 Contributing](#-contributing)
|
||||
- [📞 Support](#-support)
|
||||
|
||||
---
|
||||
|
||||
## ✨ Features
|
||||
|
||||
### 🎭 Live Data Streaming
|
||||
- **Full body tracking**: Complete skeletal animation data
|
||||
- **Facial animation**: 52 blendshapes for detailed facial expressions
|
||||
- **Finger tracking**: Precise hand and finger movements with SmartGloves
|
||||
- **Multi-actor support**: Up to 5 actors simultaneously
|
||||
- **Camera data**: Live camera tracking information
|
||||
- **Props tracking**: Real-time prop animation
|
||||
|
||||
### 🎮 Studio Integration
|
||||
- **Remote control**: Control Rokoko Studio directly from Blender
|
||||
- **Real-time preview**: See your animations as they're captured
|
||||
- **Seamless workflow**: No need to switch between applications
|
||||
|
||||
### 🔄 Animation Tools
|
||||
- **Smart retargeting**: Easily retarget animations between different rigs
|
||||
- **Auto-detection**: Automatic bone mapping for faster setup
|
||||
|
||||
## 📋 Requirements
|
||||
|
||||
| Component | Version | Notes |
|
||||
|-------------------------|------------------------------|-----------------------------------|
|
||||
| **Blender** | 2.80 or higher | Required for plugin compatibility |
|
||||
| **Rokoko Studio** | 2.4.8 or higher | For livestreaming functionality |
|
||||
| **Internet Connection** | Required during installation | To download required libraries |
|
||||
|
||||
## 📦 Installation
|
||||
|
||||
### Step 1: Download
|
||||
**[📥 Download the latest version here](https://github.com/Rokoko/rokoko-studio-live-blender/archive/refs/heads/master.zip)**
|
||||
|
||||
### Step 2: Install in Blender
|
||||
1. Open Blender
|
||||
2. Navigate to `Edit > Preferences > Addons`
|
||||
3. Click `Install...`
|
||||
4. Select the downloaded ZIP file
|
||||
5. Enable the "Rokoko Studio Live" addon
|
||||
|
||||
> ⚠️ **Note**: First-time installation may take several minutes as required libraries are downloaded.
|
||||
|
||||
### Step 3: Access the Plugin
|
||||
- In the 3D viewport, press `N` to open the sidebar
|
||||
- Select the **"Rokoko"** tab to access all plugin features
|
||||
|
||||
## 🎬 Getting Started for Streaming
|
||||
|
||||
### 1. Prepare Your Model
|
||||
|
||||
#### Character Setup
|
||||
Your character in Blender must be in **T-pose** for proper retargeting:
|
||||
|
||||
<div align="center">
|
||||
<img src="https://i.imgur.com/p4uVZBx.png" height="450" alt="Character in T-pose"/>
|
||||
</div>
|
||||
|
||||
#### SmartGloves Setup
|
||||
For optimal finger animation retargeting, ensure your character's hands match this pose:
|
||||
- All fingers should be straight
|
||||
- Thumb rotated 45 degrees away from other fingers
|
||||
|
||||
<div align="center">
|
||||
<img src="https://i.imgur.com/9I13bHI.png" alt="Hand pose for SmartGloves"/>
|
||||
</div>
|
||||
|
||||
### 2. Enable Rokoko Studio Live
|
||||
|
||||
1. Open **Rokoko Studio** and open a project
|
||||
2. On the right side, go to **Streaming**
|
||||
3. In the Blender row, press the **wrench icon** to open settings
|
||||
4. Enable **Include Connection**
|
||||
- (optional) Customize Forward IP and Port
|
||||
5. **Activate** the data stream to Blender
|
||||
|
||||
<div align="center">
|
||||
<p float="left">
|
||||
<img src="https://i.imgur.com/mkVr39B.gif" height="400" alt="Studio Live settings"/>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
### 3. Receive Data in Blender
|
||||
|
||||
1. In the **3D viewport**, press `N` to open the sidebar
|
||||
2. Select the **"Rokoko"** tab
|
||||
3. Click **"Start Receiver"** to begin receiving data from Rokoko Studio
|
||||
|
||||
<div align="center">
|
||||
<p float="left">
|
||||
<img src="https://s3.amazonaws.com/cdn.freshdesk.com/data/helpdesk/attachments/production/47010394035/original/F9BVdJ-P3GjPAqGsOno-it18A0lvyF3n3A.png" height="300" alt="Open sidebar"/>
|
||||
<img src="https://s3.amazonaws.com/cdn.freshdesk.com/data/helpdesk/attachments/production/47010394045/original/1E4Pt708FhhoGngovjP7V3CYVaNgNG_J_w.png" height="300" alt="Rokoko tab"/>
|
||||
<img src="https://s3.amazonaws.com/cdn.freshdesk.com/data/helpdesk/attachments/production/47010394056/original/Um5r_amKNoEJaF8vjF1JgQwVyjztGDtJ5w.png" height="300" alt="Start receiver"/>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
### 4. Stream Character Data
|
||||
|
||||
1. **Select the armature** you want to animate
|
||||
2. Go to **Object Properties** → **"Rokoko Studio Live Setup"** panel
|
||||
3. In the **Actor field**, select the Smartsuit for this armature
|
||||
4. Click **"Auto Detect"** to fill bone fields automatically
|
||||
5. Verify all bones are correctly mapped (fill missing bones manually if needed)
|
||||
6. Ensure the armature is in **T-Pose**, then click **"Set as T-Pose"**
|
||||
|
||||
<div align="center">
|
||||
<img src="https://i.imgur.com/ydn6cAi.gif" alt="Character setup process"/>
|
||||
</div>
|
||||
|
||||
7. **Done!** Your armature should now be animated by live data:
|
||||
|
||||
<div align="center">
|
||||
<img src="https://s3.amazonaws.com/cdn.freshdesk.com/data/helpdesk/attachments/production/47011948259/original/JDKx_BMV2iDNhqyEk1nsNqsm8zQt2YbT5g.gif" height="400" alt="Live animation"/>
|
||||
</div>
|
||||
|
||||
#### Performance Tips
|
||||
|
||||
💡 **Optimize performance** by enabling "Hide Meshes during Play" in the receiver panel:
|
||||
|
||||
<div align="center">
|
||||
<img src="https://i.imgur.com/HESveWD.png" alt="Hide meshes option"/>
|
||||
</div>
|
||||
|
||||
⚠️ **Reduce lag** by closing keyframe windows (timeline, action editor) as they can consume significant resources in earlier Blender versions.
|
||||
|
||||
### 5. Stream Face and Prop Data
|
||||
|
||||
Face and prop data follow the same workflow as character data:
|
||||
|
||||
1. **For face data**: Select the face mesh
|
||||
2. **For prop data**: Select the prop object
|
||||
3. Follow the same setup steps as character data above
|
||||
4. **Done!** Your face mesh or prop will be animated by live data
|
||||
|
||||
<div align="center">
|
||||
<p float="left">
|
||||
<img src="https://s3.amazonaws.com/cdn.freshdesk.com/data/helpdesk/attachments/production/47011946440/original/-2ES8ffaPb-jANEBaZWpLzvoy6gDB_FPXQ.gif" height="350" alt="Face animation"/>
|
||||
<img src="https://s3.amazonaws.com/cdn.freshdesk.com/data/helpdesk/attachments/production/47011950531/original/LB3AZ4q5IIPOX-WF1mYuuRqeNsWsGY_hgw.gif" height="350" alt="Prop animation"/>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
#### Custom Scaling for Props
|
||||
|
||||
💡 **For prop data**: Enable "Use Custom Scale" to adjust animation scale for your Blender project:
|
||||
|
||||
<div align="center">
|
||||
<img src="https://s3.amazonaws.com/cdn.freshdesk.com/data/helpdesk/attachments/production/47011950790/original/vpwUqdfTZJcBryvKjJmUfV0BXKT3kX__eQ.PNG" alt="Custom scale option"/>
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Retargeting
|
||||
|
||||
Easily retarget animations between different character rigs using the built-in retargeting system.
|
||||
|
||||
### Step-by-Step Process
|
||||
|
||||
1. **Open the Retargeting panel** in the Rokoko tab
|
||||
|
||||
<div align="center">
|
||||
<img src="https://s3.amazonaws.com/cdn.freshdesk.com/data/helpdesk/attachments/production/47029758599/original/gt30hHJ2JCfKDmmALDxjffiHbYjqFMQFmg.png" alt="Retargeting panel"/>
|
||||
</div>
|
||||
|
||||
2. **Set up source and target armatures**:
|
||||
- **Source armature**: Select the armature with existing animation
|
||||
- **Target armature**: Select the armature that should receive the animation
|
||||
- Click **"Build Bone List"**
|
||||
|
||||
<div align="center">
|
||||
<img src="https://s3.amazonaws.com/cdn.freshdesk.com/data/helpdesk/attachments/production/47029758649/original/AuSYaHVCMTAQmTYRX8JHohflx4B6tu7EVQ.png" alt="Build bone list"/>
|
||||
</div>
|
||||
|
||||
3. **Verify bone mapping**: Check that bones are correctly mapped and fix any missing or incorrect mappings
|
||||
|
||||
<div align="center">
|
||||
<img src="https://s3.amazonaws.com/cdn.freshdesk.com/data/helpdesk/attachments/production/47029758669/original/O_kTjk6qEKnNr_jOmvMXa2OI5d561ttBqA.png" alt="Bone mapping"/>
|
||||
</div>
|
||||
|
||||
4. **Configure retargeting options**:
|
||||
- Enable **"Auto Scale"** if armatures differ in size (or adjust manually)
|
||||
- Select the appropriate pose in **"Use Pose"**
|
||||
- ⚠️ **Important**: Ensure both armatures are in the same pose for accurate retargeting
|
||||
|
||||
5. **Execute retargeting**: Click **"Retarget Animation"**
|
||||
|
||||
6. **Done!** Your animation is now retargeted to the new armature
|
||||
|
||||
### 📺 Video Tutorial
|
||||
|
||||
<div align="center">
|
||||
|
||||
[](https://youtu.be/Od8Ecr70A4Q)
|
||||
|
||||
*Click to watch the complete retargeting tutorial*
|
||||
|
||||
</div>
|
||||
---
|
||||
|
||||
## 💡 Tips & Troubleshooting
|
||||
|
||||
### Performance Optimization
|
||||
- **Hide meshes during playback**: Enable "Hide Meshes during Play" for better performance
|
||||
- **Close keyframe panels**: Timeline and Action Editor consume significant resources
|
||||
- **Reduce viewport complexity**: Hide unnecessary objects during streaming
|
||||
|
||||
### Common Issues
|
||||
|
||||
#### Connection Problems
|
||||
- **Check network settings**: Ensure Rokoko Studio and Blender are on the same network
|
||||
- **Verify port settings**: Default port is 14043, ensure it's not blocked by firewall
|
||||
- **Restart both applications**: Sometimes a fresh start resolves connection issues
|
||||
|
||||
#### Animation Issues
|
||||
- **T-pose requirement**: Always ensure your character is in T-pose before setup
|
||||
- **Bone mapping**: Verify all bones are correctly mapped using "Auto Detect"
|
||||
- **Scale differences**: Use "Auto Scale" for characters of different sizes
|
||||
|
||||
#### Performance Issues
|
||||
- **System requirements**: Ensure your system meets minimum requirements
|
||||
- **Background processes**: Close unnecessary applications during streaming
|
||||
- **Blender settings**: Reduce viewport samples and disable unnecessary addons
|
||||
|
||||
---
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
We welcome contributions to improve the Rokoko Studio Live Plugin! Here's how you can help:
|
||||
|
||||
### Reporting Issues
|
||||
- Use the [GitHub Issues](https://github.com/Rokoko/rokoko-studio-live-blender/issues) page
|
||||
- Provide detailed information about your setup and the issue
|
||||
- Include steps to reproduce the problem
|
||||
|
||||
### Feature Requests
|
||||
- Submit feature requests through GitHub Issues
|
||||
- Describe the use case and expected behavior
|
||||
- Check existing issues to avoid duplicates
|
||||
|
||||
### Development
|
||||
- Fork the repository
|
||||
- Create a feature branch
|
||||
- Submit a pull request with detailed description
|
||||
|
||||
---
|
||||
|
||||
## 📞 Support
|
||||
|
||||
### Official Support Channels
|
||||
- **📖 Documentation**: [docs.rokoko.com](https://support.rokoko.com/hc/en-us/categories/4410420388113-Rokoko-Plugins)
|
||||
- **💬 Support Portal**: [support.rokoko.com](https://support.rokoko.com/)
|
||||
- **🐛 Bug Reports**: [GitHub Issues](https://github.com/Rokoko/rokoko-studio-live-blender/issues)
|
||||
|
||||
### Community
|
||||
- **Discord**: Join our [Discord community](https://discord.com/invite/AfCJBBQqRm)
|
||||
- **YouTube**: [Rokoko YouTube Channel](https://www.youtube.com/@RokokoMotion) for tutorials
|
||||
- **Social Media**: Follow [@rokoko](https://x.com/hellorokoko) for updates
|
||||
|
||||
---
|
||||
|
||||
<div align="center">
|
||||
|
||||
**Made with ❤️ by [Rokoko](https://www.rokoko.com/)**
|
||||
|
||||
*Bringing motion capture to everyone*
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,199 @@
|
||||
# Important plugin info for Blender
|
||||
bl_info = {
|
||||
'name': 'Rokoko Studio Live for Blender',
|
||||
'author': 'Rokoko Electronics ApS',
|
||||
'category': 'Animation',
|
||||
'location': 'View 3D > Tool Shelf > Rokoko',
|
||||
'description': 'Stream your Rokoko Studio animations directly into Blender',
|
||||
'version': (1, 4, 2),
|
||||
'blender': (2, 80, 0),
|
||||
'wiki_url': 'https://github.com/Rokoko/rokoko-studio-live-blender#readme',
|
||||
}
|
||||
|
||||
beta_branch = False
|
||||
|
||||
first_startup = "bpy" not in locals()
|
||||
import bpy
|
||||
import sys
|
||||
|
||||
# Load the updater. Important to do early, so it is always loaded even in case of an error
|
||||
from . import updater_ops
|
||||
from . import updater
|
||||
if not first_startup:
|
||||
import importlib
|
||||
importlib.reload(updater_ops)
|
||||
importlib.reload(updater)
|
||||
# Register the updater
|
||||
updater_ops.register()
|
||||
|
||||
# If first startup of this plugin, load all modules normally
|
||||
# If reloading the plugin, use importlib to reload modules
|
||||
# This lets you do adjustments to the plugin on the fly without having to restart Blender
|
||||
from . import core
|
||||
from . import panels
|
||||
from . import operators
|
||||
from . import properties
|
||||
|
||||
if first_startup:
|
||||
pass
|
||||
else:
|
||||
import importlib
|
||||
importlib.reload(core)
|
||||
importlib.reload(panels)
|
||||
importlib.reload(operators)
|
||||
importlib.reload(properties)
|
||||
|
||||
|
||||
absolute_min_ver = (2, 80, 75)
|
||||
soft_min_ver = (4, 4, 0)
|
||||
|
||||
|
||||
def check_unsupported_blender_versions():
|
||||
# Don't allow Blender versions older than 2.80
|
||||
if bpy.app.version < absolute_min_ver:
|
||||
unregister()
|
||||
sys.tracebacklimit = 0
|
||||
raise ImportError('\n\nBlender versions older than 2.80 are not supported by Rokoko Studio Live. '
|
||||
'\nPlease use Blender 2.80 or later.'
|
||||
'\n')
|
||||
|
||||
|
||||
# List of all buttons and panels
|
||||
classes_logged_in = [ # These panels will only be loaded when the user is logged in
|
||||
panels.main.ReceiverPanel,
|
||||
panels.objects.ObjectsPanel,
|
||||
panels.command_api.CommandPanel,
|
||||
panels.retargeting.RetargetingPanel,
|
||||
panels.updater.UpdaterPanel,
|
||||
panels.info.InfoPanel,
|
||||
]
|
||||
classes_logged_out = [ # These panels will only be loaded when the user is logged out
|
||||
panels.login.LoginPanel,
|
||||
panels.updater.UpdaterPanel,
|
||||
panels.info.InfoPanel,
|
||||
]
|
||||
classes_always_enable = [ # These non-panels will always be loaded, all non-panel ui should go in here
|
||||
operators.login.LoginButton,
|
||||
operators.login.LogoutButton,
|
||||
operators.login.InstallLibsButton,
|
||||
operators.receiver.ReceiverStart,
|
||||
operators.receiver.ReceiverStop,
|
||||
operators.recorder.RecorderStart,
|
||||
operators.recorder.RecorderStop,
|
||||
operators.detector.DetectFaceShapes,
|
||||
operators.detector.DetectActorBones,
|
||||
operators.detector.SaveCustomShapes,
|
||||
operators.detector.SaveCustomBones,
|
||||
operators.detector.SaveCustomBonesRetargeting,
|
||||
operators.detector.ImportCustomBones,
|
||||
operators.detector.ExportCustomBones,
|
||||
operators.detector.ClearCustomBones,
|
||||
operators.detector.ClearCustomShapes,
|
||||
operators.actor.InitTPose,
|
||||
operators.actor.ResetTPose,
|
||||
operators.actor.PrintCurrentPose,
|
||||
operators.command_api.CommandTest,
|
||||
operators.command_api.StartCalibration,
|
||||
operators.command_api.Restart,
|
||||
operators.command_api.StartRecording,
|
||||
operators.command_api.StopRecording,
|
||||
operators.retargeting.BuildBoneList,
|
||||
operators.retargeting.AddBoneListItem,
|
||||
operators.retargeting.ClearBoneList,
|
||||
operators.retargeting.RetargetAnimation,
|
||||
panels.retargeting.RSL_UL_BoneList,
|
||||
panels.retargeting.BoneListItem,
|
||||
operators.info.LicenseButton,
|
||||
operators.info.RokokoButton,
|
||||
operators.info.DocumentationButton,
|
||||
operators.info.ForumButton,
|
||||
operators.info.ToggleRokokoIDButton,
|
||||
]
|
||||
|
||||
|
||||
def register_classes(classes, unregister_classes=[]):
|
||||
# Unregister classes_logged_in first
|
||||
for cls in reversed(unregister_classes):
|
||||
try:
|
||||
bpy.utils.unregister_class(cls)
|
||||
except RuntimeError:
|
||||
print("Error: Failed to unregister class", cls)
|
||||
pass
|
||||
|
||||
register_count = 0
|
||||
for cls in classes:
|
||||
try:
|
||||
bpy.utils.register_class(cls)
|
||||
register_count += 1
|
||||
except ValueError:
|
||||
print("Error: Failed to register class", cls)
|
||||
pass
|
||||
if register_count < len(classes):
|
||||
print('Skipped', len(classes) - register_count, 'ROKOKO classes_logged_in.')
|
||||
|
||||
|
||||
def register():
|
||||
print("\n### Loading Rokoko Studio Live for Blender...")
|
||||
# Register the updater, but only if the plugin was unregistered and then registered again
|
||||
updater_ops.register()
|
||||
|
||||
# Check for unsupported Blender versions
|
||||
check_unsupported_blender_versions()
|
||||
|
||||
# Register logged out classes
|
||||
register_classes(classes_logged_out + classes_always_enable)
|
||||
|
||||
# Register all custom properties
|
||||
properties.register()
|
||||
|
||||
# Load custom icons
|
||||
core.icon_manager.load_icons()
|
||||
|
||||
# Load bone detection list
|
||||
core.detection_manager.load_detection_lists()
|
||||
|
||||
# Init fbx patcher
|
||||
core.fbx_patcher.start_fbx_patch_timer()
|
||||
|
||||
# Add info to the login user and then login if all libraries are loaded
|
||||
core.login_manager.user.set_info(classes_logged_in, classes_logged_out, bl_info)
|
||||
if core.login_manager.loaded_all_libs:
|
||||
core.login_manager.user.auto_login()
|
||||
|
||||
# Update updater info as late as possible, to ensure that errors are shown instead of being overwritten
|
||||
updater_ops.update_info(bl_info, beta_branch)
|
||||
|
||||
print("### Loaded Rokoko Studio Live for Blender successfully!\n")
|
||||
|
||||
|
||||
def unregister():
|
||||
print("### Unloading Rokoko Studio Live for Blender...")
|
||||
from . import updater_ops
|
||||
from . import operators
|
||||
from . import core
|
||||
|
||||
# Unregister updater
|
||||
updater_ops.unregister()
|
||||
|
||||
# Shut down receiver if the plugin is disabled while it is running
|
||||
if operators.receiver.receiver_enabled:
|
||||
operators.receiver.ReceiverStart.force_disable()
|
||||
|
||||
# Unregister all classes
|
||||
for cls in reversed(classes_logged_out + classes_logged_in + classes_always_enable):
|
||||
try:
|
||||
bpy.utils.unregister_class(cls)
|
||||
except RuntimeError:
|
||||
pass
|
||||
|
||||
# Unload all custom icons
|
||||
core.icon_manager.unload_icons()
|
||||
|
||||
# Exit the logged-in user
|
||||
core.login_manager.user.quit()
|
||||
|
||||
print("### Unloaded Rokoko Studio Live for Blender successfully!\n")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
register()
|
||||
@@ -0,0 +1,36 @@
|
||||
if "bpy" not in locals():
|
||||
import bpy
|
||||
from . import library_manager # has to be first, to add the lib folder to the sys.path
|
||||
|
||||
from . import receiver
|
||||
from . import animations
|
||||
from . import animation_lists
|
||||
from . import utils
|
||||
from . import state_manager
|
||||
from . import icon_manager
|
||||
from . import recorder
|
||||
from . import retargeting
|
||||
from . import detection_manager
|
||||
from . import detection_manager_v2
|
||||
from . import custom_schemes_manager
|
||||
from . import fbx_patcher
|
||||
from . import login_manager
|
||||
from . import live_data_manager
|
||||
else:
|
||||
import importlib
|
||||
importlib.reload(library_manager)
|
||||
|
||||
importlib.reload(receiver)
|
||||
importlib.reload(animations)
|
||||
importlib.reload(animation_lists)
|
||||
importlib.reload(utils)
|
||||
importlib.reload(state_manager)
|
||||
importlib.reload(icon_manager)
|
||||
importlib.reload(recorder)
|
||||
importlib.reload(retargeting)
|
||||
importlib.reload(detection_manager)
|
||||
importlib.reload(detection_manager_v2)
|
||||
importlib.reload(custom_schemes_manager)
|
||||
importlib.reload(fbx_patcher)
|
||||
importlib.reload(login_manager)
|
||||
importlib.reload(live_data_manager)
|
||||
@@ -0,0 +1,278 @@
|
||||
from mathutils import Quaternion
|
||||
from collections import OrderedDict
|
||||
|
||||
from . import animations
|
||||
|
||||
|
||||
# Face shapekeys
|
||||
face_shapes = [
|
||||
'eyeBlinkLeft',
|
||||
'eyeLookDownLeft',
|
||||
'eyeLookInLeft',
|
||||
'eyeLookOutLeft',
|
||||
'eyeLookUpLeft',
|
||||
'eyeSquintLeft',
|
||||
'eyeWideLeft',
|
||||
'eyeBlinkRight',
|
||||
'eyeLookDownRight',
|
||||
'eyeLookInRight',
|
||||
'eyeLookOutRight',
|
||||
'eyeLookUpRight',
|
||||
'eyeSquintRight',
|
||||
'eyeWideRight',
|
||||
'jawForward',
|
||||
'jawLeft',
|
||||
'jawRight',
|
||||
'jawOpen',
|
||||
'mouthClose',
|
||||
'mouthFunnel',
|
||||
'mouthPucker',
|
||||
'mouthLeft',
|
||||
'mouthRight',
|
||||
'mouthSmileLeft',
|
||||
'mouthSmileRight',
|
||||
'mouthFrownLeft',
|
||||
'mouthFrownRight',
|
||||
'mouthDimpleLeft',
|
||||
'mouthDimpleRight',
|
||||
'mouthStretchLeft',
|
||||
'mouthStretchRight',
|
||||
'mouthRollLower',
|
||||
'mouthRollUpper',
|
||||
'mouthShrugLower',
|
||||
'mouthShrugUpper',
|
||||
'mouthPressLeft',
|
||||
'mouthPressRight',
|
||||
'mouthLowerDownLeft',
|
||||
'mouthLowerDownRight',
|
||||
'mouthUpperUpLeft',
|
||||
'mouthUpperUpRight',
|
||||
'browDownLeft',
|
||||
'browDownRight',
|
||||
'browInnerUp',
|
||||
'browOuterUpLeft',
|
||||
'browOuterUpRight',
|
||||
'cheekPuff',
|
||||
'cheekSquintLeft',
|
||||
'cheekSquintRight',
|
||||
'noseSneerLeft',
|
||||
'noseSneerRight',
|
||||
'tongueOut'
|
||||
]
|
||||
|
||||
# Tpose from Studio live
|
||||
actor_bones = OrderedDict()
|
||||
actor_bones['hip'] = Quaternion((-1.0, 0.0, -0.0, 0.0))
|
||||
actor_bones['spine'] = Quaternion((-0.0, -0.0, 0.0, -1.0))
|
||||
actor_bones['chest'] = Quaternion((-0.0, -0.0, 0.0, -1.0))
|
||||
actor_bones['neck'] = Quaternion((-0.0, -0.0, 0.0, -1.0))
|
||||
actor_bones['head'] = Quaternion((-0.0, -0.0, 0.0, -1.0))
|
||||
|
||||
actor_bones['leftShoulder'] = Quaternion((-0.70711, 0.0, 0.0, 0.70711))
|
||||
actor_bones['leftUpperArm'] = Quaternion((-0.5, -0.5, -0.5, 0.5))
|
||||
actor_bones['leftLowerArm'] = Quaternion((-0.5, -0.5, -0.5, 0.5))
|
||||
actor_bones['leftHand'] = Quaternion((-0.5, -0.5, -0.5, 0.5))
|
||||
|
||||
actor_bones['rightShoulder'] = Quaternion((0.70711, 0.0, -0.0, 0.70711))
|
||||
actor_bones['rightUpperArm'] = Quaternion((0.5, 0.5, -0.5, 0.5))
|
||||
actor_bones['rightLowerArm'] = Quaternion((0.5, 0.5, -0.5, 0.5))
|
||||
actor_bones['rightHand'] = Quaternion((0.5, 0.5, -0.5, 0.5))
|
||||
|
||||
actor_bones['leftUpLeg'] = Quaternion((0.70711, -0.0, 0.70711, -0.0))
|
||||
actor_bones['leftLeg'] = Quaternion((0.70711, -0.0, 0.70711, 0.0))
|
||||
actor_bones['leftFoot'] = Quaternion((0.0, -0.0, 0.70711, -0.70711))
|
||||
actor_bones['leftToe'] = Quaternion((0.0, -0.0, 0.70711, -0.70711))
|
||||
# actor_bones['leftToeEnd'] = Quaternion((0.0, -0.0, 0.70711, -0.70711))
|
||||
|
||||
actor_bones['rightUpLeg'] = Quaternion((0.70711, -0.0, -0.70711, 0.0))
|
||||
actor_bones['rightLeg'] = Quaternion((0.70711, -0.0, -0.70711, 0.0))
|
||||
actor_bones['rightFoot'] = Quaternion((0.0, 0.0, -0.70711, 0.70711))
|
||||
actor_bones['rightToe'] = Quaternion((0.0, 0.0, -0.70711, 0.70711))
|
||||
# actor_bones['rightToeEnd'] = Quaternion((0.0, 0.0, -0.70711, 0.70711))
|
||||
|
||||
# actor_bones['leftThumbProximal'] = Quaternion((-0.0923, -0.56098, -0.70106, 0.43046))
|
||||
# actor_bones['leftThumbMedial'] = Quaternion((-0.2706, -0.65328, -0.65328, 0.2706))
|
||||
# actor_bones['leftThumbDistal'] = Quaternion((-0.2706, -0.65328, -0.65328, 0.2706))
|
||||
# # actor_bones['leftThumbTip'] = Quaternion((-0.2706, -0.65328, -0.65328, 0.2706))
|
||||
#
|
||||
# actor_bones['leftIndexProximal'] = Quaternion((-0.5, -0.5, -0.5, 0.5))
|
||||
# actor_bones['leftIndexMedial'] = Quaternion((-0.5, -0.5, -0.5, 0.5))
|
||||
# actor_bones['leftIndexDistal'] = Quaternion((-0.5, -0.5, -0.5, 0.5))
|
||||
# # actor_bones['leftIndexTip'] = Quaternion((-0.5, -0.5, -0.5, 0.5))
|
||||
#
|
||||
# actor_bones['leftMiddleProximal'] = Quaternion((-0.5, -0.5, -0.5, 0.5))
|
||||
# actor_bones['leftMiddleMedial'] = Quaternion((-0.5, -0.5, -0.5, 0.5))
|
||||
# actor_bones['leftMiddleDistal'] = Quaternion((-0.5, -0.5, -0.5, 0.5))
|
||||
# # actor_bones['leftMiddleTip'] = Quaternion((-0.5, -0.5, -0.5, 0.5))
|
||||
#
|
||||
# actor_bones['leftRingProximal'] = Quaternion((-0.5, -0.5, -0.5, 0.5))
|
||||
# actor_bones['leftRingMedial'] = Quaternion((-0.5, -0.5, -0.5, 0.5))
|
||||
# actor_bones['leftRingDistal'] = Quaternion((-0.5, -0.5, -0.5, 0.5))
|
||||
# # actor_bones['leftRingTip'] = Quaternion((-0.5, -0.5, -0.5, 0.5))
|
||||
#
|
||||
# actor_bones['leftLittleProximal'] = Quaternion((-0.5, -0.5, -0.5, 0.5))
|
||||
# actor_bones['leftLittleMedial'] = Quaternion((-0.5, -0.5, -0.5, 0.5))
|
||||
# actor_bones['leftLittleDistal'] = Quaternion((-0.5, -0.5, -0.5, 0.5))
|
||||
# # actor_bones['leftLittleTip'] = Quaternion((-0.5, -0.5, -0.5, 0.5))
|
||||
#
|
||||
# actor_bones['rightThumbProximal'] = Quaternion((0.0923, 0.56099, -0.70106, 0.43046))
|
||||
# actor_bones['rightThumbMedial'] = Quaternion((0.2706, 0.65328, -0.65328, 0.2706))
|
||||
# actor_bones['rightThumbDistal'] = Quaternion((0.2706, 0.65328, -0.65328, 0.2706))
|
||||
# # actor_bones['rightThumbTip'] = Quaternion((0.2706, 0.65328, -0.65328, 0.2706))
|
||||
#
|
||||
# actor_bones['rightIndexProximal'] = Quaternion((0.5, 0.5, -0.5, 0.5))
|
||||
# actor_bones['rightIndexMedial'] = Quaternion((0.5, 0.5, -0.5, 0.5))
|
||||
# actor_bones['rightIndexDistal'] = Quaternion((0.5, 0.5, -0.5, 0.5))
|
||||
# # actor_bones['rightIndexTip'] = Quaternion((0.5, 0.5, -0.5, 0.5))
|
||||
#
|
||||
# actor_bones['rightMiddleProximal'] = Quaternion((0.5, 0.5, -0.5, 0.5))
|
||||
# actor_bones['rightMiddleMedial'] = Quaternion((0.5, 0.5, -0.5, 0.5))
|
||||
# actor_bones['rightMiddleDistal'] = Quaternion((0.5, 0.5, -0.5, 0.5))
|
||||
# # actor_bones['rightMiddleTip'] = Quaternion((0.5, 0.5, -0.5, 0.5))
|
||||
#
|
||||
# actor_bones['rightRingProximal'] = Quaternion((0.5, 0.5, -0.5, 0.5))
|
||||
# actor_bones['rightRingMedial'] = Quaternion((0.5, 0.5, -0.5, 0.5))
|
||||
# actor_bones['rightRingDistal'] = Quaternion((0.5, 0.5, -0.5, 0.5))
|
||||
# # actor_bones['rightRingTip'] = Quaternion((0.5, 0.5, -0.5, 0.5))
|
||||
#
|
||||
# actor_bones['rightLittleProximal'] = Quaternion((0.5, 0.5, -0.5, 0.5))
|
||||
# actor_bones['rightLittleMedial'] = Quaternion((0.5, 0.5, -0.5, 0.5))
|
||||
# actor_bones['rightLittleDistal'] = Quaternion((0.5, 0.5, -0.5, 0.5))
|
||||
# # actor_bones['rightLittleTip'] = Quaternion((0.5, 0.5, -0.5, 0.5))
|
||||
|
||||
|
||||
glove_bones = OrderedDict()
|
||||
glove_bones['hip'] = Quaternion((-1.0, 0.0, -0.0, 0.0))
|
||||
glove_bones['spine'] = Quaternion((-0.0, -0.0, 0.0, -1.0))
|
||||
glove_bones['chest'] = Quaternion((-0.0, -0.0, 0.0, -1.0))
|
||||
glove_bones['neck'] = Quaternion((-0.0, -0.0, 0.0, -1.0))
|
||||
glove_bones['head'] = Quaternion((-0.0, -0.0, 0.0, -1.0))
|
||||
|
||||
glove_bones['leftShoulder'] = Quaternion((-0.70711, 0.0, 0.0, 0.70711))
|
||||
glove_bones['leftUpperArm'] = Quaternion((-0.5, -0.5, -0.5, 0.5))
|
||||
glove_bones['leftLowerArm'] = Quaternion((-0.5, -0.5, -0.5, 0.5))
|
||||
glove_bones['leftHand'] = Quaternion((-0.5, -0.5, -0.5, 0.5))
|
||||
|
||||
glove_bones['rightShoulder'] = Quaternion((0.70711, 0.0, -0.0, 0.70711))
|
||||
glove_bones['rightUpperArm'] = Quaternion((0.5, 0.5, -0.5, 0.5))
|
||||
glove_bones['rightLowerArm'] = Quaternion((0.5, 0.5, -0.5, 0.5))
|
||||
glove_bones['rightHand'] = Quaternion((0.5, 0.5, -0.5, 0.5))
|
||||
|
||||
glove_bones['leftUpLeg'] = Quaternion((0.70711, -0.0, 0.70711, -0.0))
|
||||
glove_bones['leftLeg'] = Quaternion((0.70711, -0.0, 0.70711, 0.0))
|
||||
glove_bones['leftFoot'] = Quaternion((0.0, -0.0, 0.70711, -0.70711))
|
||||
glove_bones['leftToe'] = Quaternion((0.0, -0.0, 0.70711, -0.70711))
|
||||
# glove_bones['leftToeEnd'] = Quaternion((0.0, -0.0, 0.70711, -0.70711))
|
||||
|
||||
glove_bones['rightUpLeg'] = Quaternion((0.70711, -0.0, -0.70711, 0.0))
|
||||
glove_bones['rightLeg'] = Quaternion((0.70711, -0.0, -0.70711, 0.0))
|
||||
glove_bones['rightFoot'] = Quaternion((0.0, 0.0, -0.70711, 0.70711))
|
||||
glove_bones['rightToe'] = Quaternion((0.0, 0.0, -0.70711, 0.70711))
|
||||
# glove_bones['rightToeEnd'] = Quaternion((0.0, 0.0, -0.70711, 0.70711))
|
||||
|
||||
|
||||
glove_bones['leftThumbProximal'] = Quaternion((-0.0923, -0.56098, -0.70106, 0.43046))
|
||||
glove_bones['leftThumbMedial'] = Quaternion((-0.2706, -0.65328, -0.65328, 0.2706))
|
||||
glove_bones['leftThumbDistal'] = Quaternion((-0.2706, -0.65328, -0.65328, 0.2706))
|
||||
# glove_bones['leftThumbTip'] = Quaternion((-0.2706, -0.65328, -0.65328, 0.2706))
|
||||
|
||||
glove_bones['leftIndexProximal'] = Quaternion((-0.5, -0.5, -0.5, 0.5))
|
||||
glove_bones['leftIndexMedial'] = Quaternion((-0.5, -0.5, -0.5, 0.5))
|
||||
glove_bones['leftIndexDistal'] = Quaternion((-0.5, -0.5, -0.5, 0.5))
|
||||
# glove_bones['leftIndexTip'] = Quaternion((-0.5, -0.5, -0.5, 0.5))
|
||||
|
||||
glove_bones['leftMiddleProximal'] = Quaternion((-0.5, -0.5, -0.5, 0.5))
|
||||
glove_bones['leftMiddleMedial'] = Quaternion((-0.5, -0.5, -0.5, 0.5))
|
||||
glove_bones['leftMiddleDistal'] = Quaternion((-0.5, -0.5, -0.5, 0.5))
|
||||
# glove_bones['leftMiddleTip'] = Quaternion((-0.5, -0.5, -0.5, 0.5))
|
||||
|
||||
glove_bones['leftRingProximal'] = Quaternion((-0.5, -0.5, -0.5, 0.5))
|
||||
glove_bones['leftRingMedial'] = Quaternion((-0.5, -0.5, -0.5, 0.5))
|
||||
glove_bones['leftRingDistal'] = Quaternion((-0.5, -0.5, -0.5, 0.5))
|
||||
# glove_bones['leftRingTip'] = Quaternion((-0.5, -0.5, -0.5, 0.5))
|
||||
|
||||
glove_bones['leftLittleProximal'] = Quaternion((-0.5, -0.5, -0.5, 0.5))
|
||||
glove_bones['leftLittleMedial'] = Quaternion((-0.5, -0.5, -0.5, 0.5))
|
||||
glove_bones['leftLittleDistal'] = Quaternion((-0.5, -0.5, -0.5, 0.5))
|
||||
# glove_bones['leftLittleTip'] = Quaternion((-0.5, -0.5, -0.5, 0.5))
|
||||
|
||||
glove_bones['rightThumbProximal'] = Quaternion((0.0923, 0.56099, -0.70106, 0.43046))
|
||||
glove_bones['rightThumbMedial'] = Quaternion((0.2706, 0.65328, -0.65328, 0.2706))
|
||||
glove_bones['rightThumbDistal'] = Quaternion((0.2706, 0.65328, -0.65328, 0.2706))
|
||||
# glove_bones['rightThumbTip'] = Quaternion((0.2706, 0.65328, -0.65328, 0.2706))
|
||||
|
||||
glove_bones['rightIndexProximal'] = Quaternion((0.5, 0.5, -0.5, 0.5))
|
||||
glove_bones['rightIndexMedial'] = Quaternion((0.5, 0.5, -0.5, 0.5))
|
||||
glove_bones['rightIndexDistal'] = Quaternion((0.5, 0.5, -0.5, 0.5))
|
||||
# glove_bones['rightIndexTip'] = Quaternion((0.5, 0.5, -0.5, 0.5))
|
||||
|
||||
glove_bones['rightMiddleProximal'] = Quaternion((0.5, 0.5, -0.5, 0.5))
|
||||
glove_bones['rightMiddleMedial'] = Quaternion((0.5, 0.5, -0.5, 0.5))
|
||||
glove_bones['rightMiddleDistal'] = Quaternion((0.5, 0.5, -0.5, 0.5))
|
||||
# glove_bones['rightMiddleTip'] = Quaternion((0.5, 0.5, -0.5, 0.5))
|
||||
|
||||
glove_bones['rightRingProximal'] = Quaternion((0.5, 0.5, -0.5, 0.5))
|
||||
glove_bones['rightRingMedial'] = Quaternion((0.5, 0.5, -0.5, 0.5))
|
||||
glove_bones['rightRingDistal'] = Quaternion((0.5, 0.5, -0.5, 0.5))
|
||||
# glove_bones['rightRingTip'] = Quaternion((0.5, 0.5, -0.5, 0.5))
|
||||
|
||||
glove_bones['rightLittleProximal'] = Quaternion((0.5, 0.5, -0.5, 0.5))
|
||||
glove_bones['rightLittleMedial'] = Quaternion((0.5, 0.5, -0.5, 0.5))
|
||||
glove_bones['rightLittleDistal'] = Quaternion((0.5, 0.5, -0.5, 0.5))
|
||||
# glove_bones['rightLittleTip'] = Quaternion((0.5, 0.5, -0.5, 0.5))
|
||||
|
||||
|
||||
def get_bones(with_gloves=True):
|
||||
"""
|
||||
Return all bones with their default positions. This represents the exact default position of the Studio reference avatar
|
||||
:param with_gloves: Determines if the hand bones should be included or not
|
||||
:return: dictionary with bone names and default positions
|
||||
"""
|
||||
# TODO: Remove redundant entries and create dicts at the start of the plugin
|
||||
return glove_bones if with_gloves else actor_bones
|
||||
|
||||
|
||||
# Creates the list of props and trackers for the objects panel
|
||||
def get_props_trackers(self, context):
|
||||
choices = [('None', '-None-', 'None')]
|
||||
|
||||
for prop in animations.live_data.props:
|
||||
# 1. Will be returned by context.scene
|
||||
# 2. Will be shown in lists
|
||||
# 3. will be shown in the hover description (below description)
|
||||
prop_name = animations.live_data.get_prop_name(prop)
|
||||
choices.append((animations.live_data.get_prop_id(prop), prop_name, prop_name))
|
||||
|
||||
for tracker in animations.live_data.trackers:
|
||||
tracker_name = animations.live_data.get_prop_name(tracker, is_tracker=True)
|
||||
choices.append((animations.live_data.get_prop_id(tracker, is_tracker=True), tracker_name, tracker_name))
|
||||
|
||||
return choices
|
||||
|
||||
|
||||
# Creates the list of faces for the objects panel
|
||||
def get_faces(self, context):
|
||||
choices = [('None', '-None-', 'None')]
|
||||
|
||||
for face in animations.live_data.faces:
|
||||
# 1. Will be returned by context.scene
|
||||
# 2. Will be shown in lists
|
||||
# 3. will be shown in the hover description (below description)
|
||||
face_id = animations.live_data.get_face_id(face)
|
||||
choices.append((face_id, face_id, face_id))
|
||||
|
||||
return choices
|
||||
|
||||
|
||||
# Creates the list of actors for the objects panel
|
||||
def get_actors(self, context):
|
||||
choices = [('None', '-None-', 'None')]
|
||||
|
||||
for actor in animations.live_data.actors:
|
||||
# 1. Will be returned by context.scene
|
||||
# 2. Will be shown in lists
|
||||
# 3. will be shown in the hover description (below description)
|
||||
actor_id = animations.live_data.get_actor_id(actor)
|
||||
choices.append((actor_id, actor_id, actor_id))
|
||||
|
||||
return choices
|
||||
@@ -0,0 +1,236 @@
|
||||
import bpy
|
||||
from mathutils import Quaternion, Matrix
|
||||
|
||||
from . import animation_lists, recorder
|
||||
from .live_data_manager import LiveData
|
||||
|
||||
live_data: LiveData = LiveData()
|
||||
|
||||
|
||||
def clear_animations():
|
||||
live_data.clear_data()
|
||||
|
||||
|
||||
def animate():
|
||||
|
||||
for obj in bpy.data.objects:
|
||||
# Animate all trackers and props
|
||||
if live_data.props or live_data.trackers:
|
||||
animate_tracker_prop(obj)
|
||||
|
||||
# Animate all faces
|
||||
if obj.type == 'MESH' and live_data.faces:
|
||||
animate_face(obj)
|
||||
|
||||
# Animate all actors
|
||||
elif obj.type == 'ARMATURE':
|
||||
if live_data.actors:
|
||||
animate_actor(obj)
|
||||
|
||||
|
||||
def animate_tracker_prop(obj):
|
||||
if not obj.rsl_animations_props_trackers or obj.rsl_animations_props_trackers == 'None':
|
||||
return
|
||||
|
||||
# Get prop
|
||||
prop = live_data.get_prop_by_obj(obj)
|
||||
if not prop:
|
||||
return
|
||||
|
||||
# Get the scene scaling
|
||||
scene_scale = bpy.context.scene.rsl_scene_scaling
|
||||
if obj.rsl_use_custom_scale:
|
||||
scene_scale = obj.rsl_custom_scene_scale
|
||||
|
||||
# Set the transforms of the object
|
||||
obj.rotation_mode = 'QUATERNION'
|
||||
obj.location = pos_studio_to_blender(
|
||||
prop['position']['x'] * scene_scale,
|
||||
prop['position']['y'] * scene_scale,
|
||||
prop['position']['z'] * scene_scale,
|
||||
)
|
||||
obj.rotation_quaternion = rot_studio_to_blender(
|
||||
prop['rotation']['w'],
|
||||
prop['rotation']['x'],
|
||||
prop['rotation']['y'],
|
||||
prop['rotation']['z'],
|
||||
)
|
||||
|
||||
# Record data
|
||||
if bpy.context.scene.rsl_recording:
|
||||
recorder.record_object(live_data.timestamp, obj.name, obj.rotation_quaternion, obj.location)
|
||||
|
||||
|
||||
def animate_face(obj):
|
||||
if not hasattr(obj.data, 'shape_keys') or not hasattr(obj.data.shape_keys, 'key_blocks'):
|
||||
return
|
||||
if not obj.rsl_animations_faces or obj.rsl_animations_faces == 'None':
|
||||
return
|
||||
|
||||
# Get the face live data
|
||||
face = live_data.get_face_by_obj(obj)
|
||||
if not face:
|
||||
return
|
||||
|
||||
# Set each assigned shapekey to the value of it's according live data value
|
||||
for shapekey_name in animation_lists.face_shapes:
|
||||
# Get assigned shapekey
|
||||
shapekey = obj.data.shape_keys.key_blocks.get(getattr(obj, 'rsl_face_' + shapekey_name))
|
||||
if shapekey:
|
||||
shapekey.slider_min = -1
|
||||
shapekey.value = face[shapekey_name] / 100
|
||||
|
||||
if bpy.context.scene.rsl_recording:
|
||||
# shapekey.keyframe_insert(data_path='value', group=obj.name)
|
||||
recorder.record_face(live_data.timestamp, obj.name, shapekey_name, shapekey.value)
|
||||
|
||||
|
||||
def animate_actor(obj):
|
||||
# Return if no actor is assigned to this object
|
||||
if not obj.rsl_animations_actors or obj.rsl_animations_actors == 'None':
|
||||
return
|
||||
|
||||
# Get the actor data assigned to the object
|
||||
actor = live_data.get_actor_by_obj(obj)
|
||||
if not actor:
|
||||
return
|
||||
|
||||
# Get current custom data from this object
|
||||
# The models t-pose bone rotations and locations, which are set by the user, are stored inside this custom data
|
||||
custom_data = obj.get('CUSTOM')
|
||||
if not custom_data:
|
||||
# print('NO CUSTOM DATA')
|
||||
return
|
||||
|
||||
# Get tpose data from custom data
|
||||
tpose_bones = custom_data.get('rsl_tpose_bones')
|
||||
if not tpose_bones:
|
||||
# print('NO TPOSE DATA')
|
||||
return
|
||||
|
||||
# Go over every mapped bone and animate it
|
||||
# bone_name: Name if the bone
|
||||
# studio_reference_tpose_rot: Studios reference t-pose rotation (still in Studio space)
|
||||
for bone_name, studio_reference_tpose_rot in animation_lists.get_bones(with_gloves=live_data.has_gloves(actor)).items():
|
||||
|
||||
# Gets the name of the bone assigned to this bone live data
|
||||
bone_name_assigned = getattr(obj, 'rsl_actor_' + bone_name)
|
||||
|
||||
# Gets the assigned pose bone and it's tpose data set by the user
|
||||
bone = obj.pose.bones.get(bone_name_assigned)
|
||||
bone_data = obj.data.bones.get(bone_name_assigned)
|
||||
bone_tpose_data = tpose_bones.get(bone_name_assigned)
|
||||
|
||||
try:
|
||||
actor_bone_data = actor[bone_name] if live_data.version <= 2 else actor['body'][bone_name]
|
||||
except KeyError:
|
||||
print('Bone not found in live data:', bone_name)
|
||||
continue
|
||||
|
||||
# Skip if there is no bone assigned to this live data or if there is no tpose data for this bone
|
||||
if not bone or not bone_tpose_data:
|
||||
continue
|
||||
|
||||
# Set the bones rotation mode to euler and disable inherit rotation
|
||||
if bone.rotation_mode == 'QUATERNION':
|
||||
bone.rotation_mode = 'XYZ'
|
||||
bone_data.use_inherit_rotation = False
|
||||
|
||||
# The global rotation of the models t-pose, which was set by the user
|
||||
bone_tpose_rot_global = Quaternion(bone_tpose_data['rotation_global'])
|
||||
|
||||
# The new pose in which the bone should be (still in Studio space)
|
||||
studio_new_pose = Quaternion((
|
||||
float(actor_bone_data['rotation']['w']),
|
||||
float(actor_bone_data['rotation']['x']),
|
||||
float(actor_bone_data['rotation']['y']),
|
||||
float(actor_bone_data['rotation']['z']),
|
||||
))
|
||||
|
||||
# Function to convert from Studio to Blender space
|
||||
def rot_to_blender(rot):
|
||||
return Quaternion((
|
||||
rot.w,
|
||||
rot.x,
|
||||
-rot.y,
|
||||
-rot.z,
|
||||
)) @ Quaternion((0, 0, 0, 1))
|
||||
|
||||
mat_obj = obj.matrix_local.decompose()[1].to_matrix().to_4x4()
|
||||
mat_default = Matrix((
|
||||
(1, 0, 0, 0),
|
||||
(0, 0, -1, 0),
|
||||
(0, 1, 0, 0),
|
||||
(0, 0, 0, 1)
|
||||
))
|
||||
rot_transform = (mat_default.inverted() @ mat_obj).to_quaternion()
|
||||
|
||||
def transform(rot):
|
||||
return rot_transform @ rot
|
||||
|
||||
def transform_back(rot):
|
||||
return rot_transform.inverted() @ rot
|
||||
|
||||
# Transform rotation matrix of tpose to target space
|
||||
bone_tpose_rot_global = transform(bone_tpose_rot_global)
|
||||
|
||||
# Calculate bone offset from tpose and add it to live data rotation
|
||||
rot_offset_ref = rot_to_blender(studio_reference_tpose_rot).inverted() @ bone_tpose_rot_global
|
||||
final_rot = rot_to_blender(studio_new_pose) @ rot_offset_ref
|
||||
|
||||
# Transform rotation matrix back from target space
|
||||
final_rot = transform_back(final_rot)
|
||||
|
||||
# Set new bone rotation
|
||||
orig_loc, _, _ = bone.matrix.decompose()
|
||||
orig_loc_mat = Matrix.Translation(orig_loc)
|
||||
rotation_mat = final_rot.to_matrix().to_4x4()
|
||||
|
||||
# Set final bone matrix
|
||||
bone.matrix = orig_loc_mat @ rotation_mat
|
||||
|
||||
# If hips bone, set its position
|
||||
if bone_name == 'hip':
|
||||
# Get correct space of hips location
|
||||
axis = 0
|
||||
multiplier = 1
|
||||
if round(mat_obj[2][0], 0) == round(mat_obj[2][2], 0) == 0:
|
||||
axis = 1
|
||||
multiplier = mat_obj[2][1]
|
||||
if round(mat_obj[2][0], 0) == round(mat_obj[2][1], 0) == 0:
|
||||
axis = 2
|
||||
multiplier = mat_obj[2][2]
|
||||
|
||||
# Get scale of studio model
|
||||
studio_hip_height = actor.get('hipHeight') if live_data.version <= 2 else actor.get('dimensions').get('hipHeight')
|
||||
if not studio_hip_height:
|
||||
studio_hip_height = 1
|
||||
|
||||
tpose_hip_location_y = bone_tpose_data['location_object'][axis] * multiplier
|
||||
|
||||
location_new = pos_hips_studio_to_blender(
|
||||
actor_bone_data['position']['x'] * tpose_hip_location_y / studio_hip_height,
|
||||
actor_bone_data['position']['y'] * tpose_hip_location_y - tpose_hip_location_y * studio_hip_height,
|
||||
actor_bone_data['position']['z'] * tpose_hip_location_y / studio_hip_height)
|
||||
|
||||
bone.location = location_new
|
||||
|
||||
# Record the data
|
||||
if bpy.context.scene.rsl_recording:
|
||||
recorder.record_bone(live_data.timestamp, obj.name, bone_name_assigned, bone.rotation_euler, location=bone.location if bone_name == 'hip' else None)
|
||||
|
||||
|
||||
def animate_glove(obj):
|
||||
pass
|
||||
|
||||
|
||||
def pos_hips_studio_to_blender(x, y, z):
|
||||
return -x, y, z
|
||||
|
||||
|
||||
def pos_studio_to_blender(x, y, z):
|
||||
return -x, -z, y
|
||||
|
||||
|
||||
def rot_studio_to_blender(w, x, y, z):
|
||||
return w, x, z, -y
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,55 @@
|
||||
|
||||
shape_list = {
|
||||
'eyeBlinkLeft': [],
|
||||
'eyeLookDownLeft': [],
|
||||
'eyeLookInLeft': [],
|
||||
'eyeLookOutLeft': [],
|
||||
'eyeLookUpLeft': [],
|
||||
'eyeSquintLeft': [],
|
||||
'eyeWideLeft': [],
|
||||
'eyeBlinkRight': [],
|
||||
'eyeLookDownRight': [],
|
||||
'eyeLookInRight': [],
|
||||
'eyeLookOutRight': [],
|
||||
'eyeLookUpRight': [],
|
||||
'eyeSquintRight': [],
|
||||
'eyeWideRight': [],
|
||||
'jawForward': [],
|
||||
'jawLeft': [],
|
||||
'jawRight': [],
|
||||
'jawOpen': [],
|
||||
'mouthClose': [],
|
||||
'mouthFunnel': [],
|
||||
'mouthPucker': [],
|
||||
'mouthLeft': [],
|
||||
'mouthRight': [],
|
||||
'mouthSmileLeft': [],
|
||||
'mouthSmileRight': [],
|
||||
'mouthFrownLeft': [],
|
||||
'mouthFrownRight': [],
|
||||
'mouthDimpleLeft': [],
|
||||
'mouthDimpleRight': [],
|
||||
'mouthStretchLeft': [],
|
||||
'mouthStretchRight': [],
|
||||
'mouthRollLower': [],
|
||||
'mouthRollUpper': [],
|
||||
'mouthShrugLower': [],
|
||||
'mouthShrugUpper': [],
|
||||
'mouthPressLeft': [],
|
||||
'mouthPressRight': [],
|
||||
'mouthLowerDownLeft': [],
|
||||
'mouthLowerDownRight': [],
|
||||
'mouthUpperUpLeft': [],
|
||||
'mouthUpperUpRight': [],
|
||||
'browDownLeft': [],
|
||||
'browDownRight': [],
|
||||
'browInnerUp': [],
|
||||
'browOuterUpLeft': [],
|
||||
'browOuterUpRight': [],
|
||||
'cheekPuff': [],
|
||||
'cheekSquintLeft': [],
|
||||
'cheekSquintRight': [],
|
||||
'noseSneerLeft': [],
|
||||
'noseSneerRight': [],
|
||||
'tongueOut': [],
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
import os
|
||||
import bpy
|
||||
import json
|
||||
import pathlib
|
||||
|
||||
from . import retargeting
|
||||
from . import detection_manager
|
||||
|
||||
main_dir = str(pathlib.Path(os.path.dirname(__file__)).parent.resolve())
|
||||
resources_dir = os.path.join(main_dir, "resources")
|
||||
custom_bones_dir = os.path.join(resources_dir, "custom_bones")
|
||||
custom_bone_list_file = os.path.join(custom_bones_dir, "custom_bone_list.json")
|
||||
|
||||
|
||||
def save_retargeting_to_list():
|
||||
armature_target = retargeting.get_target_armature()
|
||||
retargeting_dict = detection_manager.detect_retarget_bones()
|
||||
|
||||
for bone_item in bpy.context.scene.rsl_retargeting_bone_list:
|
||||
if not bone_item.bone_name_source or not bone_item.bone_name_target:
|
||||
continue
|
||||
|
||||
bone_name_key = bone_item.bone_name_key
|
||||
bone_name_source = bone_item.bone_name_source.lower()
|
||||
bone_name_target = bone_item.bone_name_target.lower()
|
||||
bone_name_target_detected, bone_name_key_detected = retargeting_dict[bone_item.bone_name_source]
|
||||
|
||||
if bone_name_target_detected == bone_item.bone_name_target:
|
||||
continue
|
||||
|
||||
if bone_name_key_detected and bone_name_key_detected != 'spine':
|
||||
if not detection_manager.bone_detection_list_custom.get(bone_name_key_detected):
|
||||
detection_manager.bone_detection_list_custom[bone_name_key_detected] = []
|
||||
|
||||
# TODO Idea: If a target bone got detected but was removed and left empty, add it to an ignore list. So if that exact match-up gets detected again, leave it empty
|
||||
|
||||
# If the detected target is in the custom bones list but it got changed, remove it from the list. If the new bone gets detected automatically now, don't add it to the custom list
|
||||
if bone_name_target_detected.lower() in detection_manager.bone_detection_list_custom[bone_name_key_detected]:
|
||||
if bone_name_key_detected.startswith('custom_bone_') and len(detection_manager.bone_detection_list_custom[bone_name_key_detected]) == 2:
|
||||
detection_manager.bone_detection_list_custom.pop(bone_name_key_detected)
|
||||
else:
|
||||
detection_manager.bone_detection_list_custom[bone_name_key_detected].remove(bone_name_target_detected.lower())
|
||||
|
||||
# Update the bone detection list in order to correctly figure out if the new selected bone needs to be saved
|
||||
detection_manager.bone_detection_list = detection_manager.combine_lists(detection_manager.bone_detection_list_unmodified, detection_manager.bone_detection_list_custom)
|
||||
|
||||
retargeting_dict = detection_manager.detect_retarget_bones()
|
||||
bone_name_detected_new, _ = retargeting_dict[bone_item.bone_name_source]
|
||||
if bone_name_detected_new.lower() == bone_name_target:
|
||||
# print('No need to add new bone to save')
|
||||
continue
|
||||
|
||||
# If the source bone got detected but the target bone got changed, save the target bone into the custom list
|
||||
if bone_name_target not in detection_manager.bone_detection_list_custom[bone_name_key_detected]:
|
||||
detection_manager.bone_detection_list_custom[bone_name_key_detected] = [bone_name_target] + detection_manager.bone_detection_list_custom[bone_name_key_detected]
|
||||
continue
|
||||
|
||||
# If it is a completely new pair of bones or a spine bone, add it as a new bone to the list
|
||||
detection_manager.bone_detection_list_custom['custom_bone_' + bone_name_source] = [bone_name_source, bone_name_target]
|
||||
|
||||
# Save the updated custom list locally and update
|
||||
save_to_file_and_update()
|
||||
|
||||
|
||||
def save_live_data_bone_to_list(bone_key, bone_name, bone_name_previous):
|
||||
if not detection_manager.bone_detection_list_custom.get(bone_key):
|
||||
detection_manager.bone_detection_list_custom[bone_key] = []
|
||||
|
||||
# If the previously detected bone name is in the custom bones list but it got changed, remove it from the list. If the new bone gets detected automatically now, don't add it to the custom list
|
||||
if bone_name_previous.lower() in detection_manager.bone_detection_list_custom[bone_key]:
|
||||
detection_manager.bone_detection_list_custom[bone_key].remove(bone_name_previous.lower())
|
||||
# print('Removed:', bone_name_previous)
|
||||
|
||||
# Update the bone detection list in order to correctly figure out if the new selected bone needs to be saved
|
||||
detection_manager.bone_detection_list = detection_manager.combine_lists(detection_manager.bone_detection_list_unmodified, detection_manager.bone_detection_list_custom)
|
||||
|
||||
bone_name_detected_new = detection_manager.detect_bone(bpy.context.active_object, bone_key)
|
||||
if bone_name_detected_new == bone_name:
|
||||
# print('No need to add new bone to save')
|
||||
return
|
||||
|
||||
detection_manager.bone_detection_list_custom[bone_key] = [bone_name] + detection_manager.bone_detection_list_custom[bone_key]
|
||||
|
||||
|
||||
def save_live_data_shape_to_list(shape_key, shape_name, shape_name_previous):
|
||||
|
||||
if not detection_manager.shape_detection_list_custom.get(shape_key):
|
||||
detection_manager.shape_detection_list_custom[shape_key] = []
|
||||
|
||||
# If the previously detected shape name is in the custom shapes list but it got changed, remove it from the list. If the new shapekey gets detected automatically now, don't add it to the custom list
|
||||
if shape_name_previous.lower() in detection_manager.shape_detection_list_custom[shape_key]:
|
||||
detection_manager.shape_detection_list_custom[shape_key].remove(shape_name_previous.lower())
|
||||
# print('Removed:', shape_name_previous)
|
||||
|
||||
# Update the shapekey detection list in order to correctly figure out if the new selected shapekey needs to be saved
|
||||
detection_manager.shape_detection_list = detection_manager.combine_lists(detection_manager.shape_detection_list_unmodified, detection_manager.shape_detection_list_custom)
|
||||
|
||||
shape_name_detected_new = detection_manager.detect_shape(bpy.context.active_object, shape_key)
|
||||
if shape_name_detected_new == shape_name:
|
||||
# print('No need to add new bone to save')
|
||||
return
|
||||
|
||||
detection_manager.shape_detection_list_custom[shape_key] = [shape_name] + detection_manager.shape_detection_list_custom[shape_key]
|
||||
|
||||
|
||||
def save_to_file_and_update():
|
||||
save_custom_to_file()
|
||||
detection_manager.load_detection_lists()
|
||||
|
||||
|
||||
def save_custom_to_file(file_path=custom_bone_list_file):
|
||||
new_custom_list = clean_custom_list()
|
||||
print('To File:', new_custom_list)
|
||||
|
||||
if not os.path.isdir(custom_bones_dir):
|
||||
os.mkdir(custom_bones_dir)
|
||||
|
||||
with open(file_path, 'w', encoding="utf8") as outfile:
|
||||
json.dump(new_custom_list, outfile, ensure_ascii=False, indent=4)
|
||||
|
||||
|
||||
def load_custom_lists_from_file(file_path=custom_bone_list_file):
|
||||
custom_bone_list = {}
|
||||
try:
|
||||
with open(file_path, encoding="utf8") as file:
|
||||
custom_bone_list = json.load(file)
|
||||
except FileNotFoundError:
|
||||
print('Custom bone list not found.')
|
||||
except json.decoder.JSONDecodeError:
|
||||
print("Custom bone list is not a valid json file!")
|
||||
|
||||
if custom_bone_list.get('rokoko_custom_names') is None or custom_bone_list.get('version') is None or custom_bone_list.get('bones') is None or custom_bone_list.get('shapes') is None:
|
||||
print("Custom name list file is not a valid name list file")
|
||||
return {}, {}
|
||||
|
||||
custom_bone_list.pop('rokoko_custom_names')
|
||||
custom_bone_list.pop('version')
|
||||
|
||||
return custom_bone_list.get('bones'), custom_bone_list.get('shapes')
|
||||
|
||||
|
||||
def clean_custom_list():
|
||||
new_custom_list = {
|
||||
'rokoko_custom_names': True,
|
||||
'version': 1,
|
||||
'bones': {},
|
||||
'shapes': {}
|
||||
}
|
||||
|
||||
new_bone_list = {}
|
||||
new_shape_list = {}
|
||||
|
||||
# Remove all empty fields and make all custom fields lowercase
|
||||
for key, values in detection_manager.bone_detection_list_custom.items():
|
||||
if not values:
|
||||
continue
|
||||
|
||||
for i in range(len(values)):
|
||||
values[i] = values[i].lower()
|
||||
|
||||
new_bone_list[key] = values
|
||||
|
||||
# Remove all empty fields and make all custom fields lowercase
|
||||
for key, values in detection_manager.shape_detection_list_custom.items():
|
||||
if not values:
|
||||
continue
|
||||
|
||||
for i in range(len(values)):
|
||||
values[i] = values[i].lower()
|
||||
|
||||
new_shape_list[key] = values
|
||||
|
||||
new_custom_list['bones'] = new_bone_list
|
||||
new_custom_list['shapes'] = new_shape_list
|
||||
|
||||
return new_custom_list
|
||||
|
||||
|
||||
def import_custom_list(directory, file_name):
|
||||
file_path = os.path.join(directory, file_name)
|
||||
new_custom_bone_list, new_custom_shape_list = load_custom_lists_from_file(file_path=file_path)
|
||||
|
||||
# Merge the new and old custom bone lists
|
||||
for key, bones in detection_manager.bone_detection_list_custom.items():
|
||||
if not new_custom_bone_list.get(key):
|
||||
new_custom_bone_list[key] = []
|
||||
|
||||
for bone in new_custom_bone_list[key]:
|
||||
if bone in bones:
|
||||
bones.remove(bone)
|
||||
|
||||
new_custom_bone_list[key] += bones
|
||||
|
||||
# Merge the new and old custom shape lists
|
||||
for key, shapes in detection_manager.shape_detection_list_custom.items():
|
||||
if not new_custom_shape_list.get(key):
|
||||
new_custom_shape_list[key] = []
|
||||
|
||||
for shape in new_custom_shape_list[key]:
|
||||
if shape in shapes:
|
||||
shapes.remove(shape)
|
||||
|
||||
new_custom_shape_list[key] += shapes
|
||||
|
||||
detection_manager.bone_detection_list_custom = new_custom_bone_list
|
||||
detection_manager.shape_detection_list_custom = new_custom_shape_list
|
||||
|
||||
|
||||
def export_custom_list2(directory):
|
||||
file_path = os.path.join(directory, 'custom_bone_list.json')
|
||||
|
||||
i = 1
|
||||
while os.path.isfile(file_path):
|
||||
file_path = os.path.join(directory, 'custom_bone_list' + str(i) + '.json')
|
||||
i += 1
|
||||
|
||||
save_custom_to_file(file_path=file_path)
|
||||
|
||||
return os.path.basename(file_path)
|
||||
|
||||
|
||||
def export_custom_list(file_path):
|
||||
if not detection_manager.bone_detection_list_custom and not detection_manager.shape_detection_list_custom:
|
||||
return None
|
||||
|
||||
save_custom_to_file(file_path=file_path)
|
||||
return os.path.basename(file_path)
|
||||
|
||||
|
||||
def delete_custom_bone_list():
|
||||
detection_manager.bone_detection_list_custom = {}
|
||||
save_to_file_and_update()
|
||||
|
||||
|
||||
def delete_custom_shape_list():
|
||||
detection_manager.shape_detection_list_custom = {}
|
||||
save_to_file_and_update()
|
||||
@@ -0,0 +1,386 @@
|
||||
import os
|
||||
import bpy
|
||||
import json
|
||||
import pathlib
|
||||
|
||||
from . import retargeting
|
||||
from .auto_detect_lists.bones import bone_list, ignore_rokoko_retargeting_bones
|
||||
from .auto_detect_lists.shapes import shape_list
|
||||
from .custom_schemes_manager import load_custom_lists_from_file
|
||||
|
||||
bone_detection_list = {}
|
||||
bone_detection_list_unmodified = {}
|
||||
bone_detection_list_custom = {}
|
||||
shape_detection_list = {}
|
||||
shape_detection_list_unmodified = {}
|
||||
shape_detection_list_custom = {}
|
||||
|
||||
main_dir = str(pathlib.Path(os.path.dirname(__file__)).parent.resolve())
|
||||
resources_dir = os.path.join(main_dir, "resources")
|
||||
custom_bones_dir = os.path.join(resources_dir, "custom_bones")
|
||||
custom_bone_list_file = os.path.join(custom_bones_dir, "custom_bone_list.json")
|
||||
|
||||
|
||||
def load_detection_lists():
|
||||
global bone_detection_list, bone_detection_list_unmodified, bone_detection_list_custom, shape_detection_list, shape_detection_list_unmodified, shape_detection_list_custom
|
||||
|
||||
# Create the lists from the internal naming lists
|
||||
bone_detection_list_unmodified = setup_bone_list(bone_list)
|
||||
shape_detection_list_unmodified = setup_shape_list()
|
||||
|
||||
# Load the custom naming lists from the file
|
||||
bone_detection_list_custom, shape_detection_list_custom = load_custom_lists_from_file()
|
||||
|
||||
# Combine custom and internal lists
|
||||
bone_detection_list = combine_lists(bone_detection_list_unmodified, bone_detection_list_custom)
|
||||
shape_detection_list = combine_lists(shape_detection_list_unmodified, shape_detection_list_custom)
|
||||
|
||||
# Print the whole bone list
|
||||
# print_bone_detection_list()
|
||||
|
||||
|
||||
def setup_bone_list(raw_bone_list):
|
||||
new_bone_list = {}
|
||||
|
||||
for bone_key, bone_values in raw_bone_list.items():
|
||||
# Add the bones to the list if no side indicator is found
|
||||
if 'left' not in bone_key:
|
||||
new_bone_list[bone_key] = [bone_value.lower() for bone_value in bone_values]
|
||||
if bone_key == 'spine':
|
||||
new_bone_list['chest'] = [bone_value.lower() for bone_value in bone_values]
|
||||
continue
|
||||
|
||||
# Add bones to the list that are two sided
|
||||
bone_values_left = []
|
||||
bone_values_right = []
|
||||
|
||||
for bone_name in bone_values:
|
||||
bone_name = bone_name.lower()
|
||||
|
||||
if '\l' in bone_name:
|
||||
for replacement in ['l', 'left', 'r', 'right']:
|
||||
bone_name_new = bone_name.replace('\l', replacement)
|
||||
|
||||
# Debug if duplicates are found
|
||||
if bone_name_new in bone_values_left or bone_name_new in bone_values_right:
|
||||
print('Duplicate autodetect bone entry:', bone_name, bone_name_new)
|
||||
continue
|
||||
|
||||
if 'l' in replacement:
|
||||
bone_values_left.append(bone_name_new)
|
||||
else:
|
||||
bone_values_right.append(bone_name_new)
|
||||
|
||||
bone_key_left = bone_key
|
||||
bone_key_right = bone_key.replace('left', 'right')
|
||||
|
||||
new_bone_list[bone_key_left] = bone_values_left
|
||||
new_bone_list[bone_key_right] = bone_values_right
|
||||
|
||||
return new_bone_list
|
||||
|
||||
|
||||
def setup_shape_list():
|
||||
new_shape_list = {}
|
||||
|
||||
for shape_key, shape_names in shape_list.items():
|
||||
new_shape_list[shape_key] = [shape_key.lower()] + [shape_name.lower() for shape_name in shape_names]
|
||||
|
||||
return new_shape_list
|
||||
|
||||
|
||||
def combine_lists(internal_list, custom_list):
|
||||
"""
|
||||
Creates a combined list with the second list put in first but with the structure of the first list
|
||||
"""
|
||||
combined_list = {}
|
||||
|
||||
# Set dictionary structure
|
||||
for key in internal_list.keys():
|
||||
combined_list[key] = []
|
||||
|
||||
# Load in custom values into the dictionary
|
||||
for key, values in custom_list.items():
|
||||
combined_list[key] = []
|
||||
for value in values:
|
||||
combined_list[key].append(value.lower())
|
||||
|
||||
# Load in internal values
|
||||
for key, values in internal_list.items():
|
||||
for value in values:
|
||||
combined_list[key].append(value)
|
||||
|
||||
return combined_list
|
||||
|
||||
|
||||
def print_bone_detection_list():
|
||||
print('BONES')
|
||||
for key, values in bone_detection_list.items():
|
||||
print(key, values)
|
||||
print()
|
||||
|
||||
print('CUSTOM BONES')
|
||||
for key, values in bone_detection_list_custom.items():
|
||||
print(key, values)
|
||||
print('--> ', bone_detection_list[key])
|
||||
print()
|
||||
|
||||
# print('SHAPES')
|
||||
# for key, values in shape_detection_list.items():
|
||||
# print(key, values)
|
||||
|
||||
print('CUSTOM SHAPES')
|
||||
for key, values in shape_detection_list_custom.items():
|
||||
print(key, values)
|
||||
print('--> ', shape_detection_list[key])
|
||||
print()
|
||||
print()
|
||||
|
||||
|
||||
# def get_bone_list():
|
||||
# return bone_detection_list
|
||||
#
|
||||
#
|
||||
# def get_custom_bone_list():
|
||||
# return bone_detection_list_custom
|
||||
#
|
||||
#
|
||||
# def get_shape_list():
|
||||
# return shape_detection_list
|
||||
#
|
||||
#
|
||||
# def get_custom_shape_list():
|
||||
# return shape_detection_list_custom
|
||||
|
||||
|
||||
def standardize_bone_name(name):
|
||||
# List of chars to replace if they are at the start of a bone name
|
||||
starts_with = [
|
||||
('_', ''),
|
||||
('ValveBiped_', ''),
|
||||
('Valvebiped_', ''),
|
||||
('Bip1_', 'Bip_'),
|
||||
('Bip01_', 'Bip_'),
|
||||
('Bip001_', 'Bip_'),
|
||||
('Character1_', ''),
|
||||
('HLP_', ''),
|
||||
('JD_', ''),
|
||||
('JU_', ''),
|
||||
('Armature|', ''),
|
||||
('Bone_', ''),
|
||||
('C_', ''),
|
||||
('Cf_S_', ''),
|
||||
('Cf_J_', ''),
|
||||
('G_', ''),
|
||||
('Joint_', ''),
|
||||
('DEF_', ''),
|
||||
('CC_Base_', ''),
|
||||
]
|
||||
|
||||
# Standardize names
|
||||
# Make all the underscores!
|
||||
name = name.replace(' ', '_') \
|
||||
.replace('-', '_') \
|
||||
.replace('.', '_') \
|
||||
.replace('____', '_') \
|
||||
.replace('___', '_') \
|
||||
.replace('__', '_') \
|
||||
|
||||
# Replace if name starts with specified chars
|
||||
for replacement in starts_with:
|
||||
if name.startswith(replacement[0]):
|
||||
name = replacement[1] + name[len(replacement[0]):]
|
||||
|
||||
# Remove digits from the start
|
||||
name_split = name.split('_')
|
||||
if len(name_split) > 1 and name_split[0].isdigit():
|
||||
name = name_split[1]
|
||||
|
||||
# Specific condition
|
||||
name_split = name.split('"')
|
||||
if len(name_split) > 3:
|
||||
name = name_split[1]
|
||||
|
||||
# Another specific condition
|
||||
if ':' in name:
|
||||
for i, split in enumerate(name.split(':')):
|
||||
if i == 0:
|
||||
name = ''
|
||||
else:
|
||||
name += split
|
||||
|
||||
# Remove S0 from the end
|
||||
if name[-2:] == 'S0':
|
||||
name = name[:-2]
|
||||
|
||||
if name[-4:] == '_Jnt':
|
||||
name = name[:-4]
|
||||
|
||||
return name.lower()
|
||||
|
||||
|
||||
def detect_shape(obj, shape_name_key):
|
||||
# Go through the target mesh and search for shapekey that fit the main shapekey
|
||||
found_shape_name = ''
|
||||
is_custom = False
|
||||
|
||||
for shapekey in obj.data.shape_keys.key_blocks:
|
||||
if is_custom: # If a custom shapekey name was found, stop searching. it has priority
|
||||
break
|
||||
|
||||
if shape_detection_list_custom.get(shape_name_key):
|
||||
for shape_name_detected in shape_detection_list_custom[shape_name_key]:
|
||||
if shape_name_detected == shapekey.name.lower():
|
||||
found_shape_name = shapekey.name
|
||||
is_custom = True
|
||||
break
|
||||
|
||||
if found_shape_name and shape_name_key != 'chest': # If a shape_name was found, only continue looking for custom shapekey names, they have priority
|
||||
continue
|
||||
|
||||
for shape_name_detected in shape_detection_list[shape_name_key]:
|
||||
if shape_name_detected == shapekey.name.lower():
|
||||
found_shape_name = shapekey.name
|
||||
break
|
||||
|
||||
# If nothing was found, check if the shapekey names match exactly
|
||||
if not found_shape_name and shape_name_key.lower() == shapekey.name.lower():
|
||||
found_shape_name = shapekey.name
|
||||
|
||||
return found_shape_name
|
||||
|
||||
|
||||
def detect_bone(obj, bone_name_key, bone_name_source=None):
|
||||
# Go through the target armature and search for bones that fit the main source bone
|
||||
found_bone_name = ''
|
||||
is_custom = False
|
||||
|
||||
if not bone_name_source:
|
||||
bone_name_source = bone_name_key
|
||||
|
||||
for bone in obj.pose.bones:
|
||||
if is_custom: # If a custom bone name was found, stop searching. it has priority
|
||||
break
|
||||
|
||||
if bone_detection_list_custom.get(bone_name_key):
|
||||
for bone_name_detected in bone_detection_list_custom[bone_name_key]:
|
||||
if bone_name_detected == bone.name.lower():
|
||||
found_bone_name = bone.name
|
||||
is_custom = True
|
||||
break
|
||||
|
||||
if found_bone_name and bone_name_key != 'chest': # If a bone_name was found, only continue looking for custom bone names, they have priority
|
||||
continue
|
||||
|
||||
for bone_name_detected in bone_detection_list[bone_name_key]:
|
||||
if bone_name_detected == standardize_bone_name(bone.name):
|
||||
found_bone_name = bone.name
|
||||
break
|
||||
|
||||
# If nothing was found, check if the bone names match exactly
|
||||
if not found_bone_name and bone_name_source.lower() == bone.name.lower():
|
||||
found_bone_name = bone.name
|
||||
|
||||
return found_bone_name
|
||||
|
||||
|
||||
def detect_retarget_bones() -> {str: (str, str)}:
|
||||
"""
|
||||
Detects all matching bones in the target and source armatures
|
||||
:return: A dictionary with the source bone name as key and a tuple of the target bone name and their shared key name as value
|
||||
"""
|
||||
bone_list_animated = []
|
||||
retargeting_dict = {}
|
||||
armature_source = retargeting.get_source_armature()
|
||||
armature_target = retargeting.get_target_armature()
|
||||
|
||||
# Get all source bones from the animation and add them to bone_list_animated
|
||||
for fc in armature_source.animation_data.action.fcurves:
|
||||
bone_name = fc.data_path.split('"')
|
||||
if len(bone_name) == 3 and bone_name[1] not in bone_list_animated:
|
||||
bone_list_animated.append(bone_name[1])
|
||||
|
||||
# Check if this animation is from Rokoko Studio. Ignore certain bones in that case
|
||||
is_rokoko_animation = False
|
||||
if 'newton' in bone_list_animated and 'RightFinger1Tip' in bone_list_animated and 'HeadVertex' in bone_list_animated and 'LeftFinger2Metacarpal' in bone_list_animated:
|
||||
is_rokoko_animation = True
|
||||
|
||||
spines_source = []
|
||||
spines_target = []
|
||||
found_main_bones = []
|
||||
|
||||
# Then add all the bones to the retargeting dictionary
|
||||
for bone_name in bone_list_animated:
|
||||
if is_rokoko_animation and bone_name in ignore_rokoko_retargeting_bones:
|
||||
continue
|
||||
|
||||
bone_item_source = bone_name
|
||||
bone_item_target = ''
|
||||
main_bone_name = ''
|
||||
standardized_bone_name_source = standardize_bone_name(bone_name)
|
||||
|
||||
# Find the main bone name (bone name key) of the source bone
|
||||
for bone_main, bone_values in bone_detection_list.items():
|
||||
if bone_main == 'chest': # Ignore chest bones, these are only used for live data
|
||||
continue
|
||||
if bone_main in found_main_bones: # Only find main bones once, except for spines
|
||||
continue
|
||||
# If the source bone name is found in the bone detection list, add its main bone name to the list of found main bones
|
||||
if bone_name.lower() in bone_values or standardized_bone_name_source in bone_values or standardized_bone_name_source == bone_main.lower():
|
||||
main_bone_name = bone_main
|
||||
if main_bone_name != 'spine': # Ignore the spine bones for now, so that it can add the custom spine bones first
|
||||
found_main_bones.append(main_bone_name)
|
||||
break
|
||||
|
||||
# Add the source bone and the main bone name to the retargeting dict with an empty targeting bone name
|
||||
retargeting_dict[bone_item_source] = ("", main_bone_name)
|
||||
|
||||
# If no main bone name was found, continue
|
||||
if not main_bone_name:
|
||||
continue
|
||||
|
||||
# If it's a spine bone, add it to the list for later fixing
|
||||
if main_bone_name == 'spine':
|
||||
spines_source.append(bone_name)
|
||||
continue
|
||||
|
||||
# If it's a custom spine/chest bone, add it to the spine list nonetheless
|
||||
custom_main_bone = main_bone_name.startswith('custom_bone_')
|
||||
if custom_main_bone and standardize_bone_name(main_bone_name.replace('custom_bone_', '')) in bone_detection_list['spine']:
|
||||
spines_source.append(bone_name)
|
||||
|
||||
# Go through the target armature and search for bones that fit the main source bone
|
||||
bone_item_target = detect_bone(armature_target, main_bone_name, bone_name_source=bone_item_source)
|
||||
|
||||
# Add the bone to the retargeting list again
|
||||
retargeting_dict[bone_item_source] = (bone_item_target, main_bone_name)
|
||||
|
||||
# Add target spines to list for later fixing
|
||||
for bone in armature_target.pose.bones:
|
||||
bone_name_standardized = standardize_bone_name(bone.name)
|
||||
if bone_name_standardized in bone_detection_list['spine']:
|
||||
spines_target.append(bone.name)
|
||||
|
||||
# Fix spine auto detection
|
||||
if spines_target and spines_source:
|
||||
# print(spines_source)
|
||||
spine_dict = {}
|
||||
|
||||
i = 0
|
||||
for spine in reversed(spines_source):
|
||||
i += 1
|
||||
if i == len(spines_target):
|
||||
break
|
||||
spine_dict[spine] = spines_target[-i]
|
||||
|
||||
spine_dict[spines_source[0]] = spines_target[0]
|
||||
|
||||
# Fill in fixed spines into unfilled matches
|
||||
for spine_source, spine_target in spine_dict.items():
|
||||
for bone_source, bone_values in retargeting_dict.items():
|
||||
bone_target, bone_key = bone_values
|
||||
if bone_source == spine_source and not bone_target:
|
||||
retargeting_dict[bone_source] = (spine_target, bone_key)
|
||||
break
|
||||
|
||||
return retargeting_dict
|
||||
@@ -0,0 +1,74 @@
|
||||
|
||||
import bpy
|
||||
|
||||
from .auto_detect_lists.bones import bone_list, ignore_rokoko_retargeting_bones
|
||||
from .auto_detect_lists.shapes import shape_list
|
||||
|
||||
|
||||
class DetectionManager:
|
||||
|
||||
def __init__(self, name_dict):
|
||||
self.name_dict_original = name_dict
|
||||
self.name_dict = {}
|
||||
|
||||
self._setup_dict()
|
||||
|
||||
def _setup_dict(self):
|
||||
for key, values in self.name_dict_original.items():
|
||||
self._add_names(key, values)
|
||||
|
||||
def _add_names(self, key, values):
|
||||
raise NotImplementedError
|
||||
|
||||
def print_dict(self):
|
||||
for key, values in self.name_dict.items():
|
||||
print(f"{key}: {values}")
|
||||
|
||||
|
||||
class BoneDetectionManager(DetectionManager):
|
||||
def _add_names(self, key, values):
|
||||
# Add names to the list without changes if the key is not sided
|
||||
if "left" not in key:
|
||||
self.name_dict[key] = [name.lower() for name in values]
|
||||
if key == "spine":
|
||||
self.name_dict["chest"] = self.name_dict[key].copy()
|
||||
return
|
||||
|
||||
# Add names to the list with changes if the key is sided
|
||||
names_left = []
|
||||
names_right = []
|
||||
|
||||
for name in values:
|
||||
name = name.lower()
|
||||
|
||||
if "\l" not in name:
|
||||
print(f"Warning: {name} from {key} does not contain a '\\l' marker")
|
||||
continue
|
||||
|
||||
for replacement in ['l', 'left', 'r', 'right']:
|
||||
name_new = name.replace("\l", replacement)
|
||||
|
||||
if name_new in names_left or name_new in names_right:
|
||||
print(f"Warning: {name_new} from {key} is already in the list")
|
||||
continue
|
||||
|
||||
if "l" in replacement:
|
||||
names_left.append(name_new)
|
||||
else:
|
||||
names_right.append(name_new)
|
||||
|
||||
self.name_dict[key] = names_left
|
||||
self.name_dict[key.replace("left", "right")] = names_right
|
||||
|
||||
|
||||
class ShapeDetectionManager(DetectionManager):
|
||||
def _add_names(self, key, values):
|
||||
self.name_dict[key] = [key] + [name.lower() for name in values]
|
||||
|
||||
|
||||
|
||||
# bones = BoneDetectionManager(bone_list)
|
||||
# bones.print_dict()
|
||||
|
||||
# shapes = ShapeDetectionManager(shape_list)
|
||||
# shapes.print_dict()
|
||||
@@ -0,0 +1,212 @@
|
||||
import bpy
|
||||
import time
|
||||
import addon_utils
|
||||
|
||||
from threading import Thread
|
||||
|
||||
if bpy.app.version < (2, 83, 17):
|
||||
from io_scene_fbx import import_fbx
|
||||
from io_scene_fbx.import_fbx import blen_read_animations_curves_iter, blen_read_object_transform_do
|
||||
|
||||
|
||||
def start_fbx_patch_timer():
|
||||
if bpy.app.version >= (2, 83, 17): # This patch is officially accepted in Blender 2.83.17, so don't patch it
|
||||
return
|
||||
|
||||
# Asynchronously start the timer looking for the right time to patch the fbx importer
|
||||
thread = Thread(target=time_fbx_patch, args=[])
|
||||
thread.start()
|
||||
|
||||
|
||||
def time_fbx_patch():
|
||||
# Wait for Blender to finish loading up
|
||||
found_scene = False
|
||||
while not found_scene:
|
||||
if hasattr(bpy.context, 'scene'):
|
||||
found_scene = True
|
||||
else:
|
||||
time.sleep(0.5)
|
||||
|
||||
# Enable fbx if it isn't enabled yet
|
||||
fbx_is_enabled = addon_utils.check('io_scene_fbx')[1]
|
||||
if not fbx_is_enabled:
|
||||
addon_utils.enable('io_scene_fbx')
|
||||
|
||||
# Patch fbx importer
|
||||
patch_fbx_importer()
|
||||
|
||||
|
||||
def patch_fbx_importer():
|
||||
import_fbx.blen_read_animations_action_item = blen_read_animations_action_item_patched
|
||||
|
||||
|
||||
# This is the modified Blender function that will replace the original one
|
||||
def blen_read_animations_action_item_patched(action, item, cnodes, fps, anim_offset):
|
||||
"""
|
||||
'Bake' loc/rot/scale into the action,
|
||||
taking any pre_ and post_ matrix into account to transform from fbx into blender space.
|
||||
"""
|
||||
from bpy.types import Object, PoseBone, ShapeKey, Material, Camera
|
||||
from itertools import chain
|
||||
|
||||
fbx_curves = []
|
||||
for curves, fbxprop in cnodes.values():
|
||||
for (fbx_acdata, _blen_data), channel in curves.values():
|
||||
fbx_curves.append((fbxprop, channel, fbx_acdata))
|
||||
|
||||
# Leave if no curves are attached (if a blender curve is attached to scale but without keys it defaults to 0).
|
||||
if len(fbx_curves) == 0:
|
||||
return
|
||||
|
||||
blen_curves = []
|
||||
props = []
|
||||
keyframes = {}
|
||||
|
||||
# Add each keyframe to the keyframe dict
|
||||
def store_keyframe(fc, frame, value):
|
||||
fc_key = (fc.data_path, fc.array_index)
|
||||
if not keyframes.get(fc_key):
|
||||
keyframes[fc_key] = []
|
||||
keyframes[fc_key].append((frame, value))
|
||||
|
||||
if isinstance(item, Material):
|
||||
grpname = item.name
|
||||
props = [("diffuse_color", 3, grpname or "Diffuse Color")]
|
||||
elif isinstance(item, ShapeKey):
|
||||
props = [(item.path_from_id("value"), 1, "Key")]
|
||||
elif isinstance(item, Camera):
|
||||
props = [(item.path_from_id("lens"), 1, "Camera")]
|
||||
else: # Object or PoseBone:
|
||||
if item.is_bone:
|
||||
bl_obj = item.bl_obj.pose.bones[item.bl_bone]
|
||||
else:
|
||||
bl_obj = item.bl_obj
|
||||
|
||||
# We want to create actions for objects, but for bones we 'reuse' armatures' actions!
|
||||
grpname = item.bl_obj.name
|
||||
|
||||
# Since we might get other channels animated in the end, due to all FBX transform magic,
|
||||
# we need to add curves for whole loc/rot/scale in any case.
|
||||
props = [(bl_obj.path_from_id("location"), 3, grpname or "Location"),
|
||||
None,
|
||||
(bl_obj.path_from_id("scale"), 3, grpname or "Scale")]
|
||||
rot_mode = bl_obj.rotation_mode
|
||||
if rot_mode == 'QUATERNION':
|
||||
props[1] = (bl_obj.path_from_id("rotation_quaternion"), 4, grpname or "Quaternion Rotation")
|
||||
elif rot_mode == 'AXIS_ANGLE':
|
||||
props[1] = (bl_obj.path_from_id("rotation_axis_angle"), 4, grpname or "Axis Angle Rotation")
|
||||
else: # Euler
|
||||
props[1] = (bl_obj.path_from_id("rotation_euler"), 3, grpname or "Euler Rotation")
|
||||
|
||||
blen_curves = [action.fcurves.new(prop, index=channel, action_group=grpname)
|
||||
for prop, nbr_channels, grpname in props for channel in range(nbr_channels)]
|
||||
|
||||
if isinstance(item, Material):
|
||||
for frame, values in blen_read_animations_curves_iter(fbx_curves, anim_offset, 0, fps):
|
||||
value = [0,0,0]
|
||||
for v, (fbxprop, channel, _fbx_acdata) in values:
|
||||
assert(fbxprop == b'DiffuseColor')
|
||||
assert(channel in {0, 1, 2})
|
||||
value[channel] = v
|
||||
|
||||
for fc, v in zip(blen_curves, value):
|
||||
store_keyframe(fc, frame, v)
|
||||
|
||||
elif isinstance(item, ShapeKey):
|
||||
for frame, values in blen_read_animations_curves_iter(fbx_curves, anim_offset, 0, fps):
|
||||
value = 0.0
|
||||
for v, (fbxprop, channel, _fbx_acdata) in values:
|
||||
assert(fbxprop == b'DeformPercent')
|
||||
assert(channel == 0)
|
||||
value = v / 100.0
|
||||
|
||||
for fc, v in zip(blen_curves, (value,)):
|
||||
store_keyframe(fc, frame, v)
|
||||
|
||||
elif isinstance(item, Camera):
|
||||
for frame, values in blen_read_animations_curves_iter(fbx_curves, anim_offset, 0, fps):
|
||||
value = 0.0
|
||||
for v, (fbxprop, channel, _fbx_acdata) in values:
|
||||
assert(fbxprop == b'FocalLength')
|
||||
assert(channel == 0)
|
||||
value = v
|
||||
|
||||
for fc, v in zip(blen_curves, (value,)):
|
||||
store_keyframe(fc, frame, v)
|
||||
|
||||
else: # Object or PoseBone:
|
||||
if item.is_bone:
|
||||
bl_obj = item.bl_obj.pose.bones[item.bl_bone]
|
||||
else:
|
||||
bl_obj = item.bl_obj
|
||||
|
||||
transform_data = item.fbx_transform_data
|
||||
rot_eul_prev = bl_obj.rotation_euler.copy()
|
||||
rot_quat_prev = bl_obj.rotation_quaternion.copy()
|
||||
|
||||
# Pre-compute inverted local rest matrix of the bone, if relevant.
|
||||
restmat_inv = item.get_bind_matrix().inverted_safe() if item.is_bone else None
|
||||
|
||||
# Create a dict to store all keyframes in order to add them later all at once
|
||||
keyframes = {}
|
||||
|
||||
for frame, values in blen_read_animations_curves_iter(fbx_curves, anim_offset, 0, fps):
|
||||
for v, (fbxprop, channel, _fbx_acdata) in values:
|
||||
if fbxprop == b'Lcl Translation':
|
||||
transform_data.loc[channel] = v
|
||||
elif fbxprop == b'Lcl Rotation':
|
||||
transform_data.rot[channel] = v
|
||||
elif fbxprop == b'Lcl Scaling':
|
||||
transform_data.sca[channel] = v
|
||||
mat, _, _ = blen_read_object_transform_do(transform_data)
|
||||
|
||||
# compensate for changes in the local matrix during processing
|
||||
if item.anim_compensation_matrix:
|
||||
mat = mat @ item.anim_compensation_matrix
|
||||
|
||||
# apply pre- and post matrix
|
||||
# post-matrix will contain any correction for lights, camera and bone orientation
|
||||
# pre-matrix will contain any correction for a parent's correction matrix or the global matrix
|
||||
if item.pre_matrix:
|
||||
mat = item.pre_matrix @ mat
|
||||
if item.post_matrix:
|
||||
mat = mat @ item.post_matrix
|
||||
|
||||
# And now, remove that rest pose matrix from current mat (also in parent space).
|
||||
if restmat_inv:
|
||||
mat = restmat_inv @ mat
|
||||
|
||||
# Now we have a virtual matrix of transform from AnimCurves, we can insert keyframes!
|
||||
loc, rot, sca = mat.decompose()
|
||||
if rot_mode == 'QUATERNION':
|
||||
if rot_quat_prev.dot(rot) < 0.0:
|
||||
rot = -rot
|
||||
rot_quat_prev = rot
|
||||
elif rot_mode == 'AXIS_ANGLE':
|
||||
vec, ang = rot.to_axis_angle()
|
||||
rot = ang, vec.x, vec.y, vec.z
|
||||
else: # Euler
|
||||
rot = rot.to_euler(rot_mode, rot_eul_prev)
|
||||
rot_eul_prev = rot
|
||||
|
||||
# Add each keyframe and its value to the keyframe dict
|
||||
for fc, value in zip(blen_curves, chain(loc, rot, sca)):
|
||||
store_keyframe(fc, frame, value)
|
||||
|
||||
# Add all keyframe points to the fcurves at once and modify them after
|
||||
for fc_key, key_values in keyframes.items():
|
||||
data_path, index = fc_key
|
||||
|
||||
# Add all keyframe points at once
|
||||
fcurve = action.fcurves.find(data_path=data_path, index=index)
|
||||
num_keys = len(key_values)
|
||||
fcurve.keyframe_points.add(num_keys)
|
||||
|
||||
# Apply values to each keyframe point
|
||||
for kf_point, v in zip(fcurve.keyframe_points, key_values):
|
||||
kf_point.co = v
|
||||
kf_point.interpolation = 'LINEAR'
|
||||
|
||||
# Since we inserted our keyframes in 'FAST' mode, we have to update the fcurves now.
|
||||
for fc in blen_curves:
|
||||
fc.update()
|
||||
@@ -0,0 +1,53 @@
|
||||
import os
|
||||
import pathlib
|
||||
from enum import Enum
|
||||
from bpy.utils import previews
|
||||
|
||||
icons = None
|
||||
|
||||
|
||||
class Icons(Enum):
|
||||
FACE = 'FACE'
|
||||
SUIT = 'SUIT'
|
||||
VP = 'VP'
|
||||
PAIRED = 'PAIRED'
|
||||
ROKOKO = 'ROKOKO'
|
||||
START_RECORDING = 'RECORD'
|
||||
STOP_RECORDING = 'STOP'
|
||||
RESTART = 'RESTART'
|
||||
CALIBRATE = 'CALIBRATE'
|
||||
STUDIO_LIVE_LOGO = 'STUDIO_LIVE_LOGO'
|
||||
|
||||
def get_icon(self):
|
||||
return icons.get(self.value).icon_id
|
||||
|
||||
|
||||
def load_icons():
|
||||
# Path to the icons folder
|
||||
# The path is calculated relative to this py file inside the addon folder
|
||||
main_dir = pathlib.Path(os.path.dirname(__file__)).parent.resolve()
|
||||
resources_dir = os.path.join(str(main_dir), "resources")
|
||||
icons_dir = os.path.join(resources_dir, "icons")
|
||||
|
||||
pcoll = previews.new()
|
||||
|
||||
# Load a preview thumbnail of a file and store in the previews collection
|
||||
pcoll.load('FACE', os.path.join(icons_dir, 'icon-row-face-32.png'), 'IMAGE')
|
||||
pcoll.load('SUIT', os.path.join(icons_dir, 'icon-row-suit-32.png'), 'IMAGE')
|
||||
pcoll.load('VP', os.path.join(icons_dir, 'icon-vp-32.png'), 'IMAGE')
|
||||
pcoll.load('PAIRED', os.path.join(icons_dir, 'icon-paired-32.png'), 'IMAGE')
|
||||
pcoll.load('ROKOKO', os.path.join(icons_dir, 'icon-rokoko-32.png'), 'IMAGE')
|
||||
pcoll.load('RECORD', os.path.join(icons_dir, 'icon-record-32.png'), 'IMAGE')
|
||||
pcoll.load('RESTART', os.path.join(icons_dir, 'icon-restart-32.png'), 'IMAGE')
|
||||
pcoll.load('STOP', os.path.join(icons_dir, 'icon-stop-white-32.png'), 'IMAGE')
|
||||
pcoll.load('CALIBRATE', os.path.join(icons_dir, 'icon-straight-pose-32.png'), 'IMAGE')
|
||||
pcoll.load('STUDIO_LIVE_LOGO', os.path.join(icons_dir, 'icon-studio-live-32.png'), 'IMAGE')
|
||||
|
||||
global icons
|
||||
icons = pcoll
|
||||
|
||||
|
||||
def unload_icons():
|
||||
global icons
|
||||
if icons:
|
||||
previews.remove(icons)
|
||||
@@ -0,0 +1,155 @@
|
||||
|
||||
import os
|
||||
import bpy
|
||||
import sys
|
||||
import json
|
||||
import shutil
|
||||
import pkgutil
|
||||
import pathlib
|
||||
import platform
|
||||
import ensurepip
|
||||
import subprocess
|
||||
|
||||
|
||||
class LibraryManager:
|
||||
os_name = platform.system()
|
||||
system_info = {
|
||||
"operating_system": os_name,
|
||||
}
|
||||
pip_is_updated = False
|
||||
|
||||
def __init__(self, libs_main_dir: pathlib.Path):
|
||||
self.libs_main_dir = libs_main_dir
|
||||
self.libs_info_file = self.libs_main_dir / ".lib_info"
|
||||
|
||||
python_ver_str = "".join([str(ver) for ver in sys.version_info[:2]])
|
||||
self.libs_dir = os.path.join(self.libs_main_dir, "python" + python_ver_str)
|
||||
|
||||
# Set python path on older Blender versions
|
||||
try:
|
||||
self.python = bpy.app.binary_path_python
|
||||
except AttributeError:
|
||||
self.python = sys.executable
|
||||
|
||||
self.check_libs_info()
|
||||
self._prepare_libraries()
|
||||
|
||||
def _prepare_libraries(self):
|
||||
# Create main library directory
|
||||
if not os.path.isdir(self.libs_main_dir):
|
||||
os.mkdir(self.libs_main_dir)
|
||||
# Create python specific library directory
|
||||
if not os.path.isdir(self.libs_dir):
|
||||
os.mkdir(self.libs_dir)
|
||||
|
||||
# Add the library path to the modules, so they can be loaded from the plugin
|
||||
if self.libs_dir not in sys.path:
|
||||
sys.path.append(self.libs_dir)
|
||||
|
||||
def install_libraries(self, required):
|
||||
missing_after_install = []
|
||||
|
||||
# Install missing libraries
|
||||
missing = [mod for mod in required if not pkgutil.find_loader(mod)]
|
||||
if missing:
|
||||
# Ensure and update pip
|
||||
self._update_pip()
|
||||
|
||||
# Install the missing libraries into the library path
|
||||
print("Installing missing libraries:", missing)
|
||||
try:
|
||||
# command = [self.python, '-m', 'pip', 'install', f"--target={str(self.libs_dir)}", "--index-url=http://pypi.python.org/simple/", "--trusted-host=pypi.python.org", *missing]
|
||||
command = [self.python, '-m', 'pip', 'install', f"--target={str(self.libs_dir)}", *missing]
|
||||
subprocess.check_call(command, stdout=subprocess.DEVNULL)
|
||||
except subprocess.CalledProcessError as e:
|
||||
print("PIP Error:", e)
|
||||
print("Installing libraries failed.")
|
||||
if self.os_name != "Windows":
|
||||
print("Retrying with sudo..")
|
||||
# command = ["sudo", self.python, '-m', 'pip', 'install', f"--target={str(self.libs_dir)}", "--index-url=http://pypi.python.org/simple/", "--trusted-host=pypi.python.org", *missing]
|
||||
command = ["sudo", self.python, '-m', 'pip', 'install', f"--target={str(self.libs_dir)}", *missing]
|
||||
subprocess.call(command, stdout=subprocess.DEVNULL)
|
||||
finally:
|
||||
# Reset console color, because it could still be colored after running pip
|
||||
print('\033[39m')
|
||||
|
||||
# Check if all library installations were successful
|
||||
missing_after_install = [mod for mod in required if not pkgutil.find_loader(mod)]
|
||||
installed_libs = [lib for lib in missing if lib not in missing_after_install]
|
||||
if missing_after_install:
|
||||
print("WARNING: Could not install the following libraries:", missing_after_install)
|
||||
if installed_libs:
|
||||
print("Successfully installed missing libraries:", installed_libs)
|
||||
|
||||
# Create library info file after all libraries are installed to ensure everything is installed correctly
|
||||
self.create_libs_info()
|
||||
|
||||
return missing_after_install
|
||||
|
||||
def check_libs_info(self):
|
||||
if not os.path.isdir(self.libs_dir):
|
||||
return
|
||||
|
||||
# If the library info file doesn't exist, delete the libs folder
|
||||
if not os.path.isfile(self.libs_info_file):
|
||||
print("Library info is missing, deleting library folder.")
|
||||
shutil.rmtree(self.libs_main_dir)
|
||||
return
|
||||
|
||||
# Read data from info file
|
||||
current_data = self.system_info
|
||||
with open(self.libs_info_file, 'r', encoding="utf8") as file:
|
||||
loaded_data = json.load(file)
|
||||
|
||||
# Compare info and delete libs folder if it doesn't match
|
||||
for key, val_current in current_data.items():
|
||||
val_loaded = loaded_data.get(key)
|
||||
if not val_loaded == val_current:
|
||||
print("Current info:", current_data)
|
||||
print("Loaded info: ", loaded_data)
|
||||
print("Library info is not matching, deleting library folder.")
|
||||
shutil.rmtree(self.libs_main_dir)
|
||||
return
|
||||
|
||||
def create_libs_info(self):
|
||||
# If the path doesn't exist or the info file already exists, don't create it
|
||||
if not os.path.isdir(self.libs_dir) or os.path.isfile(self.libs_info_file):
|
||||
return
|
||||
|
||||
# Write the current data to the info file
|
||||
with open(self.libs_info_file, 'w', encoding="utf8") as file:
|
||||
json.dump(self.system_info, file)
|
||||
|
||||
def _update_pip(self):
|
||||
if self.pip_is_updated:
|
||||
return
|
||||
|
||||
print("Ensuring pip")
|
||||
try:
|
||||
ensurepip.bootstrap()
|
||||
except subprocess.CalledProcessError as e:
|
||||
print("PIP Error:", e)
|
||||
print("Ensuring pip failed.")
|
||||
|
||||
print("Updating pip")
|
||||
try:
|
||||
# subprocess.check_call([self.python, "-m", "pip", "install", "--upgrade", "--index-url=http://pypi.python.org/simple/", "--trusted-host=pypi.python.org", "pip"])
|
||||
subprocess.check_call([self.python, "-m", "pip", "install", "--upgrade", "pip"])
|
||||
except subprocess.CalledProcessError as e:
|
||||
print("PIP Error:", e)
|
||||
print("Updating pip failed.")
|
||||
if self.os_name != "Windows":
|
||||
print("Retrying with sudo..")
|
||||
# subprocess.call(["sudo", self.python, "-m", "pip", "install", "--upgrade", "--index-url=http://pypi.python.org/simple/", "--trusted-host=pypi.python.org", "pip"])
|
||||
subprocess.call(["sudo", self.python, "-m", "pip", "install", "--upgrade", "pip"])
|
||||
finally:
|
||||
# Reset console color, because it could still be colored after running pip
|
||||
print('\033[39m')
|
||||
|
||||
self.pip_is_updated = True
|
||||
|
||||
|
||||
# Setup library path in the Blender addons directory and start library manager
|
||||
main_dir = pathlib.Path(os.path.dirname(__file__)).parent.parent
|
||||
libs_dir = main_dir / "Rokoko Libraries"
|
||||
lib_manager = LibraryManager(libs_dir)
|
||||
@@ -0,0 +1,176 @@
|
||||
import json
|
||||
|
||||
loaded_lz4 = False
|
||||
unsupported_os = False
|
||||
try:
|
||||
from lz4 import frame
|
||||
loaded_lz4 = True
|
||||
except ModuleNotFoundError:
|
||||
print("Error: LZ4 module didn't load. Unsupported OS or Python version!")
|
||||
except ImportError:
|
||||
print("Error: LZ4 module didn't load. Unsupported OS!")
|
||||
unsupported_os = True
|
||||
|
||||
|
||||
class LiveData:
|
||||
data = None
|
||||
version = 0
|
||||
|
||||
# JSON v2
|
||||
timestamp = 0
|
||||
props = []
|
||||
trackers = []
|
||||
faces = []
|
||||
actors = []
|
||||
|
||||
# JSON v3
|
||||
fps = 60
|
||||
timestamp_prev = 0
|
||||
timedelta_prev = 0
|
||||
|
||||
def init(self, data):
|
||||
self.data = data
|
||||
self._decode_data()
|
||||
self.clear_data()
|
||||
self._process_data()
|
||||
|
||||
def clear_data(self):
|
||||
self.version = 0
|
||||
|
||||
# JSON v2
|
||||
# self.timestamp = 0
|
||||
self.props = []
|
||||
self.trackers = []
|
||||
self.faces = []
|
||||
self.actors = []
|
||||
|
||||
# JSON v3
|
||||
self.fps = 60
|
||||
# self.timestamp_prev = 0
|
||||
# self.timedelta_prev = 0
|
||||
|
||||
def _decode_data(self):
|
||||
try:
|
||||
self.data = frame.decompress(self.data)
|
||||
except (RuntimeError, NameError):
|
||||
pass
|
||||
|
||||
try:
|
||||
self.data = json.loads(self.data)
|
||||
except UnicodeDecodeError as e:
|
||||
if loaded_lz4:
|
||||
raise UnicodeDecodeError
|
||||
# Raise an import error if the LZ4 module couldn't be loaded
|
||||
raise ImportError("os" if unsupported_os else "")
|
||||
|
||||
if not self.data:
|
||||
raise ValueError
|
||||
|
||||
def _process_data(self):
|
||||
self.version = self.data.get('version')
|
||||
ver_str = str(self.version).replace(".", ",")
|
||||
if ',' in ver_str:
|
||||
self.version = int(ver_str.split(',')[0])
|
||||
|
||||
# If the user selected JSON v2.5 in Studio 1, the version number is "3" but it contains the data from version 2
|
||||
# This checks if this is the case and sets the version number accordingly
|
||||
if self.version == 3 and self.data.get('trackers') is not None:
|
||||
self.version = 2
|
||||
|
||||
if not self.version or self.version < 2:
|
||||
raise TypeError
|
||||
|
||||
if self.version == 2:
|
||||
self.timestamp = self.data['timestamp']
|
||||
self.props = self.data['props']
|
||||
self.trackers = self.data['trackers']
|
||||
self.faces = self.data['faces']
|
||||
self.actors = self.data['actors']
|
||||
|
||||
else:
|
||||
self.fps = self.data['fps']
|
||||
|
||||
self.actors = self.data['scene']['actors']
|
||||
self.props = self.data['scene']['props']
|
||||
|
||||
for actor in self.actors:
|
||||
if actor['meta']["hasFace"]:
|
||||
actor['face']['parentName'] = actor['name']
|
||||
self.faces.append(actor['face'])
|
||||
|
||||
self._calc_timestamp()
|
||||
|
||||
def _calc_timestamp(self):
|
||||
timestamp_new = self.data['scene']['timestamp']
|
||||
delta = timestamp_new - self.timestamp_prev
|
||||
|
||||
if delta >= 0:
|
||||
self.timestamp += delta
|
||||
self.timestamp_prev = timestamp_new
|
||||
self.timedelta_prev = delta
|
||||
else:
|
||||
self.timestamp += self.timedelta_prev
|
||||
self.timestamp_prev = timestamp_new
|
||||
|
||||
def has_gloves(self, actor):
|
||||
return self.version >= 3 and actor.get('meta') and actor.get('meta').get('hasGloves')
|
||||
|
||||
def supports_trackers(self):
|
||||
return self.version <= 2
|
||||
|
||||
# Get data for and from the live data selection lists
|
||||
|
||||
def get_actor_by_obj(self, obj):
|
||||
actors = [actor for actor in self.actors if actor['name'] == obj.rsl_animations_actors]
|
||||
return actors[0] if actors else None
|
||||
|
||||
def get_actor_id(self, actor):
|
||||
return actor['name']
|
||||
|
||||
def get_face_by_obj(self, obj):
|
||||
face_id = 'faceId' if self.version <= 2 else 'parentName'
|
||||
faces = [face for face in self.faces if face[face_id] == obj.rsl_animations_faces]
|
||||
return faces[0] if faces else None
|
||||
|
||||
def get_face_id(self, face):
|
||||
face_id = 'faceId' if self.version <= 2 else 'parentName'
|
||||
return face[face_id]
|
||||
|
||||
def get_face_parent_id(self, face):
|
||||
face_id = 'profileName' if self.version <= 2 else 'parentName'
|
||||
return face[face_id]
|
||||
|
||||
def get_prop_by_obj(self, obj):
|
||||
if self.version <= 2:
|
||||
obj_id = obj.rsl_animations_props_trackers.split('|')
|
||||
obj_type = obj_id[0]
|
||||
obj_name = obj_id[1]
|
||||
|
||||
if obj_type == 'PR':
|
||||
props = [prop for prop in self.props if prop['name'] == obj_name]
|
||||
else:
|
||||
props = [tracker for tracker in self.trackers if tracker['name'] == obj_name]
|
||||
|
||||
return props[0] if props else None
|
||||
|
||||
props = [prop for prop in self.props if prop['name'] == obj.rsl_animations_props_trackers]
|
||||
return props[0] if props else None
|
||||
|
||||
def get_prop_id(self, prop, is_tracker=False):
|
||||
if self.version <= 2:
|
||||
return ('TR' if is_tracker else 'PR') + '|' + prop['name']
|
||||
return prop['name']
|
||||
|
||||
def get_prop_name(self, prop, is_tracker=False):
|
||||
if self.version <= 2:
|
||||
return ('Tracker: ' if is_tracker else 'Prop: ') + prop['name']
|
||||
return prop['name']
|
||||
|
||||
def get_prop_name_raw(self, prop, is_tracker=False):
|
||||
return prop['name']
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,520 @@
|
||||
|
||||
import os
|
||||
import bpy
|
||||
import ssl
|
||||
import sys
|
||||
import json
|
||||
import pathlib
|
||||
import asyncio
|
||||
import logging
|
||||
import datetime
|
||||
import requests
|
||||
import traceback
|
||||
import webbrowser
|
||||
|
||||
from .. import updater
|
||||
from .utils import ui_refresh_all, cancel_gen
|
||||
|
||||
from threading import Thread, Timer
|
||||
from contextlib import suppress
|
||||
from typing import AsyncGenerator
|
||||
from urllib.parse import urlparse
|
||||
|
||||
# Import extra libraries
|
||||
loaded_all_libs = False
|
||||
try:
|
||||
import boto3
|
||||
from gql import Client, gql
|
||||
from cryptography.fernet import Fernet
|
||||
from gql.transport.appsync_websockets import AppSyncWebsocketsTransport
|
||||
from gql.transport.appsync_auth import AppSyncApiKeyAuthentication
|
||||
from gql.transport.websockets import log as websockets_logger
|
||||
|
||||
# Set logging levels
|
||||
websockets_logger.setLevel(logging.CRITICAL)
|
||||
logging.getLogger('boto').setLevel(logging.CRITICAL)
|
||||
loaded_all_libs = True
|
||||
except ImportError as e:
|
||||
print(e)
|
||||
|
||||
|
||||
# Disable SSL
|
||||
ssl._create_default_https_context = ssl._create_unverified_context
|
||||
|
||||
|
||||
class Login:
|
||||
url = "https://rmp-gql-public.rokoko.com/graphql"
|
||||
aws_url = "wss://a4rau2yngvb7hn3y6m37e3b53u.appsync-realtime-api.us-east-1.amazonaws.com/graphql"
|
||||
api_key = "da2-pa7tlmpnvbcpdhe7l46q3eodvu"
|
||||
login_url = "https://id.rokoko.com/?request_id="
|
||||
timeout_duration = 60 # In seconds, how long the listener is waiting for the login event after opening the browser
|
||||
|
||||
def __init__(self):
|
||||
self.request_id = None
|
||||
self.session: Client
|
||||
self.results: AsyncGenerator
|
||||
self.timeout: Timer
|
||||
|
||||
def start(self):
|
||||
user.logging_in = True
|
||||
user.display_error = None
|
||||
|
||||
# Start the listener in a new thread so Blender can continue running
|
||||
listener = Thread(target=self._start_async, args=[])
|
||||
listener.start()
|
||||
|
||||
# Start the timeout thread which stops the listener after a few seconds if nothing happened
|
||||
self.timeout = Timer(self.timeout_duration, self._timeout)
|
||||
self.timeout.start()
|
||||
|
||||
def stop(self):
|
||||
pass
|
||||
|
||||
def _start_async(self):
|
||||
try:
|
||||
# Get the request id from the server and run the listener
|
||||
self._get_request_id()
|
||||
asyncio.run(self._run_listener())
|
||||
except Exception as e:
|
||||
print(traceback.format_exc())
|
||||
user.error("No internet connection..")
|
||||
|
||||
def _timeout(self):
|
||||
# If the user no longer logging in, don't timeout
|
||||
if not user.logging_in:
|
||||
return
|
||||
|
||||
# Stop the login listener
|
||||
print("Connection timeout, stopping listener..")
|
||||
asyncio.run(cancel_gen(self.results))
|
||||
|
||||
# Stopping login and updating UI to show timeout error
|
||||
user.error("Timeout, please try again.")
|
||||
print("Stopped login listener")
|
||||
|
||||
def _get_request_id(self):
|
||||
headers = {"x-api-key": self.api_key}
|
||||
query = """
|
||||
mutation {
|
||||
createRequestToken(client_id: "blender") {
|
||||
request_id
|
||||
access_token
|
||||
id_token
|
||||
refresh_token
|
||||
client_id
|
||||
created_at
|
||||
last_modified
|
||||
email
|
||||
username
|
||||
given_name
|
||||
family_name
|
||||
ttl
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
try:
|
||||
request = requests.post(self.url, json={'query': query}, headers=headers)
|
||||
except Exception as e:
|
||||
user.logging_in = False
|
||||
print("No connection to the server.")
|
||||
return
|
||||
|
||||
if request.status_code != 200:
|
||||
user.logging_in = False
|
||||
print(f"Query failed to reach the server by returning code of {request.status_code}.")
|
||||
return
|
||||
|
||||
data = request.json()
|
||||
self.request_id = data.get("data").get("createRequestToken").get("request_id")
|
||||
|
||||
def _open_website(self):
|
||||
webbrowser.open(self.login_url + self.request_id)
|
||||
|
||||
async def _run_listener(self):
|
||||
# Extract host from aws_url and create auth
|
||||
host = str(urlparse(self.aws_url).netloc)
|
||||
auth = AppSyncApiKeyAuthentication(host=host, api_key=self.api_key)
|
||||
|
||||
transport = AppSyncWebsocketsTransport(url=self.aws_url, auth=auth, ssl=ssl._create_unverified_context())
|
||||
|
||||
async with Client(transport=transport) as session:
|
||||
self.session = session
|
||||
subscription = gql(
|
||||
f"""
|
||||
subscription {{
|
||||
onTokenChange(request_id: "{self.request_id}") {{
|
||||
request_id
|
||||
access_token
|
||||
id_token
|
||||
refresh_token
|
||||
client_id
|
||||
created_at
|
||||
last_modified
|
||||
email
|
||||
username
|
||||
given_name
|
||||
family_name
|
||||
ttl
|
||||
}}
|
||||
}}
|
||||
"""
|
||||
)
|
||||
print("Waiting for login event..")
|
||||
|
||||
# Subscribe to the login event
|
||||
self.results = session.subscribe(subscription)
|
||||
|
||||
# Open the website to allow the user to login
|
||||
self._open_website()
|
||||
|
||||
with suppress(asyncio.CancelledError):
|
||||
|
||||
# Wait for the login event
|
||||
async for result in self.results:
|
||||
# Check if the correct data was returned
|
||||
data = result.get("onTokenChange")
|
||||
if data:
|
||||
if data.get("request_id") != self.request_id:
|
||||
user.error("Error, please try again.")
|
||||
print("Request ID not correct, please try again.")
|
||||
break
|
||||
print("Login successful, stopping listener..")
|
||||
user.login(data)
|
||||
user.login_cache.create_login_cache(data)
|
||||
break
|
||||
|
||||
# If another event was returned (like maintenance), stop the login
|
||||
user.error("Server error, please try again.")
|
||||
print("Server error:", result)
|
||||
break
|
||||
|
||||
# If the connection is closing by itself, cancel the timeout timer
|
||||
self.timeout.cancel()
|
||||
|
||||
|
||||
class LoginSilent:
|
||||
region = 'us-east-1'
|
||||
client_id = "39j3527cico5eicbtpjoc6627d"
|
||||
|
||||
def __init__(self):
|
||||
logging.getLogger('boto').setLevel(logging.ERROR)
|
||||
self.login()
|
||||
|
||||
def login(self):
|
||||
# Start the listener in a new thread so Blender can continue running
|
||||
thread = Thread(target=self._login_async, args=[])
|
||||
thread.start()
|
||||
|
||||
def _login_async(self):
|
||||
print("SILENT LOGIN")
|
||||
if not user.refresh_token:
|
||||
return
|
||||
|
||||
response = None
|
||||
try:
|
||||
sys.tracebacklimit = 0
|
||||
client = boto3.client("cognito-idp", region_name=self.region)
|
||||
response = client.initiate_auth(
|
||||
ClientId=self.client_id,
|
||||
AuthFlow='REFRESH_TOKEN',
|
||||
AuthParameters={
|
||||
'REFRESH_TOKEN': user.refresh_token
|
||||
},
|
||||
)
|
||||
except Exception as e:
|
||||
error_msg = str(e)
|
||||
print("\nERROR:", error_msg, "\n")
|
||||
if "NotAuthorizedException" in error_msg:
|
||||
user.logout()
|
||||
user.error("Logged out: Session expired")
|
||||
finally:
|
||||
del sys.tracebacklimit
|
||||
|
||||
# print("RESPONSE:", response)
|
||||
if not response:
|
||||
return
|
||||
|
||||
# Check response for challenge, logout if challenge detected
|
||||
# See here for challenges:
|
||||
# https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/cognito-idp.html#CognitoIdentityProvider.Client.initiate_auth
|
||||
challenge_name = response.get("ChallengeName")
|
||||
challenge_params = response.get("ChallengeParameters")
|
||||
|
||||
if challenge_name or challenge_params:
|
||||
print("ERROR: Further account managing needed!")
|
||||
user.logout()
|
||||
user.error("Logged out:", challenge_name)
|
||||
|
||||
|
||||
class User:
|
||||
classes_logged_in = []
|
||||
classes_logged_out = []
|
||||
|
||||
def __init__(self):
|
||||
self.logging_in = False
|
||||
self.login_cache = LoginCache()
|
||||
|
||||
self.logged_in = False
|
||||
self.email = None
|
||||
self.username = None # This is a unique id
|
||||
self.access_token = None # Only gets used for the MixPanel API
|
||||
self.refresh_token = None # Gets used to log in silently
|
||||
|
||||
self.display_email = False
|
||||
self.display_error = None
|
||||
|
||||
self.login_time = None
|
||||
self.version_str = "1.0.0"
|
||||
self.classes_logged_in = []
|
||||
self.classes_logged_out = []
|
||||
|
||||
def set_info(self, classes_logged_in, classes_logged_out, bl_info):
|
||||
self.classes_logged_in = classes_logged_in
|
||||
self.classes_logged_out = classes_logged_out
|
||||
self.version_str = ".".join(map(str, bl_info.get("version")))
|
||||
|
||||
def auto_login(self):
|
||||
# Check the login cache
|
||||
data = self.login_cache.get_login_cache()
|
||||
if not data:
|
||||
return False
|
||||
|
||||
self.login(data, register_classes=True)
|
||||
|
||||
if self.logged_in:
|
||||
LoginSilent()
|
||||
|
||||
return self.logged_in
|
||||
|
||||
def login(self, data, register_classes=True):
|
||||
# Collect data
|
||||
self.email = data.get("email")
|
||||
self.username = data.get("username")
|
||||
self.access_token = data.get("access_token")
|
||||
self.refresh_token = data.get("refresh_token")
|
||||
|
||||
# Check data validity
|
||||
self.logging_in = False
|
||||
self.logged_in = self.email and self.username and self.refresh_token and self.access_token
|
||||
if not self.logged_in:
|
||||
print("ERROR: Not all fields are filled:", self.email, self.username, self.refresh_token, self.access_token)
|
||||
self.error("Login failed, please try again")
|
||||
return
|
||||
|
||||
self.display_error = None
|
||||
self.login_time = datetime.datetime.utcnow().timestamp()
|
||||
|
||||
MixPanel.send_login_event()
|
||||
|
||||
if register_classes:
|
||||
self.register_classes()
|
||||
|
||||
def logout(self):
|
||||
if not self.logged_in:
|
||||
return
|
||||
|
||||
MixPanel.send_logout_event()
|
||||
|
||||
self.logged_in = False
|
||||
self.email = self.username = self.refresh_token = self.access_token = None
|
||||
|
||||
self.unregister_classes()
|
||||
self.login_cache.delete_cache()
|
||||
|
||||
def quit(self):
|
||||
MixPanel.send_logout_event()
|
||||
|
||||
def error(self, *msg):
|
||||
# Update the UI if the user is still logging in or of the error message changes
|
||||
update_ui = self.logging_in or msg != self.display_error
|
||||
|
||||
self.logging_in = False
|
||||
self.display_error = msg
|
||||
|
||||
if update_ui and not self.logged_in:
|
||||
ui_refresh_all()
|
||||
|
||||
def register_classes(self):
|
||||
# Unregister logged out classes
|
||||
for cls in reversed(self.classes_logged_out):
|
||||
bpy.utils.unregister_class(cls)
|
||||
|
||||
# Register logged in classes
|
||||
for cls in self.classes_logged_in:
|
||||
bpy.utils.register_class(cls)
|
||||
|
||||
def unregister_classes(self):
|
||||
# Unregister classes_logged_in
|
||||
for cls in reversed(self.classes_logged_in):
|
||||
bpy.utils.unregister_class(cls)
|
||||
|
||||
# Register classes_logged_out
|
||||
for cls in self.classes_logged_out:
|
||||
bpy.utils.register_class(cls)
|
||||
|
||||
|
||||
class LoginCache:
|
||||
main_dir = pathlib.Path(os.path.dirname(__file__)).parent.resolve()
|
||||
resources_dir = os.path.join(main_dir, "resources")
|
||||
cache_dir = os.path.join(resources_dir, "cache")
|
||||
cache_file = os.path.join(cache_dir, ".cache")
|
||||
key = 'p03Ab7CuvhUuwcbOU4nBAl_QkoaU8XxciKvHGb5Wfd0='
|
||||
|
||||
def __init__(self):
|
||||
self.f = None
|
||||
|
||||
def create_login_cache(self, data):
|
||||
if not self.f:
|
||||
self.f = Fernet(self.key)
|
||||
if not os.path.isdir(self.cache_dir):
|
||||
os.mkdir(self.cache_dir)
|
||||
|
||||
data_str = json.dumps(data)
|
||||
encoded_data = data_str.encode()
|
||||
encrypted_data = self.f.encrypt(encoded_data)
|
||||
|
||||
with open(self.cache_file, 'wb') as file:
|
||||
file.write(encrypted_data)
|
||||
|
||||
def get_login_cache(self):
|
||||
if not self.f:
|
||||
self.f = Fernet(self.key)
|
||||
if not os.path.isfile(self.cache_file):
|
||||
return None
|
||||
|
||||
with open(self.cache_file, 'rb') as file:
|
||||
encrypted_data = file.read()
|
||||
|
||||
# Decrypt cache data and load it as json
|
||||
encoded_data = self.f.decrypt(encrypted_data)
|
||||
data_str = encoded_data.decode()
|
||||
data = json.loads(data_str)
|
||||
|
||||
if not self.is_valid(data):
|
||||
return None
|
||||
|
||||
return data
|
||||
|
||||
def delete_cache(self):
|
||||
if os.path.isfile(self.cache_file):
|
||||
os.remove(self.cache_file)
|
||||
|
||||
def is_valid(self, data):
|
||||
if not data:
|
||||
return False
|
||||
|
||||
# Check if the cache is too old
|
||||
creation_date = data.get("created_at")
|
||||
if not creation_date:
|
||||
return False
|
||||
|
||||
duration_timestamp = int(datetime.datetime.now().timestamp()) - creation_date
|
||||
duration = datetime.timedelta(seconds=duration_timestamp)
|
||||
|
||||
if duration.days > 90:
|
||||
print("Cache too old, please login again")
|
||||
self.delete_cache()
|
||||
user.error("Login expired (90 days)")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class MixPanel:
|
||||
# url = "https://rmp-team-gql.rokoko.com/graphql"
|
||||
|
||||
url = "https://rmp-gql-public.rokoko.com/graphql"
|
||||
api_key = "da2-pa7tlmpnvbcpdhe7l46q3eodvu"
|
||||
|
||||
@staticmethod
|
||||
def send_login_event():
|
||||
if not user.username:
|
||||
return
|
||||
|
||||
headers = {"x-api-key": MixPanel.api_key}
|
||||
|
||||
event_properties = {
|
||||
"action": "login",
|
||||
"blender_version": ".".join(map(str, bpy.app.version)),
|
||||
"plugin_version": user.version_str,
|
||||
}
|
||||
event_properties = json.dumps(event_properties).replace("\"", "\\\"")
|
||||
|
||||
query = f"""
|
||||
mutation {{
|
||||
trackInMixpanel(input: {{
|
||||
event_name: "session_start"
|
||||
event_properties: "{event_properties}"
|
||||
distinct_id: "{user.username}"
|
||||
client_id: BLENDER
|
||||
}}
|
||||
)
|
||||
}}
|
||||
"""
|
||||
|
||||
try:
|
||||
request = requests.post(MixPanel.url, json={'query': query}, headers=headers)
|
||||
except Exception as e:
|
||||
user.logging_in = False
|
||||
print("No connection to the server.")
|
||||
return
|
||||
|
||||
if request.status_code != 200:
|
||||
user.logging_in = False
|
||||
print(f"Query failed to reach the server by returning code of {request.status_code}.")
|
||||
return
|
||||
|
||||
# data = request.json()
|
||||
# print("MIXPANEL LOGIN RECEIVED DATA:", data)
|
||||
|
||||
@staticmethod
|
||||
def send_logout_event():
|
||||
if not user.username:
|
||||
return
|
||||
|
||||
headers = {"x-api-key": MixPanel.api_key}
|
||||
|
||||
session_duration = 0
|
||||
if user.login_time:
|
||||
session_duration = datetime.datetime.utcnow().timestamp() - user.login_time
|
||||
session_duration = round(session_duration, 2)
|
||||
|
||||
event_properties = {
|
||||
"action": "logout",
|
||||
"blender_version": ".".join(map(str, bpy.app.version)),
|
||||
"plugin_version": user.version_str,
|
||||
"session_duration": session_duration,
|
||||
}
|
||||
event_properties = json.dumps(event_properties).replace("\"", "\\\"")
|
||||
|
||||
query = f"""
|
||||
mutation {{
|
||||
trackInMixpanel(input: {{
|
||||
event_name: "session_end"
|
||||
event_properties: "{event_properties}"
|
||||
distinct_id: "{user.username}"
|
||||
client_id: BLENDER
|
||||
}}
|
||||
)
|
||||
}}
|
||||
"""
|
||||
|
||||
try:
|
||||
request = requests.post(MixPanel.url, json={'query': query}, headers=headers)
|
||||
except Exception as e:
|
||||
user.logging_in = False
|
||||
print("No connection to the server.")
|
||||
return
|
||||
|
||||
if request.status_code != 200:
|
||||
user.logging_in = False
|
||||
print(f"Query failed to reach the server by returning code of {request.status_code}.")
|
||||
return
|
||||
|
||||
|
||||
user: User = User()
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
|
||||
import bpy
|
||||
import time
|
||||
import socket
|
||||
import traceback
|
||||
from . import animations, utils
|
||||
|
||||
error_temp = ''
|
||||
show_error = []
|
||||
|
||||
|
||||
# Starts UPD server and handles data received from Rokoko Studio
|
||||
class Receiver:
|
||||
|
||||
sock = None
|
||||
|
||||
# Redraw counters
|
||||
i = -1 # Number of continuous received packets
|
||||
i_np = 0 # Number of continuous no packets
|
||||
|
||||
# Error counters
|
||||
error_temp = []
|
||||
error_count = 0
|
||||
|
||||
def run(self):
|
||||
data_raw = None
|
||||
received = True
|
||||
error = []
|
||||
force_error = False
|
||||
|
||||
# Try to receive a packet
|
||||
try:
|
||||
data_raw, address = self.sock.recvfrom(81920) # Prev 65536
|
||||
except BlockingIOError as e:
|
||||
print('Blocking error:', e)
|
||||
error = ['Receiving no data!']
|
||||
except OSError as e:
|
||||
print('Packet error:', e.strerror)
|
||||
error = ['Packets too big!']
|
||||
force_error = True
|
||||
except AttributeError as e:
|
||||
print('Socket error:', e)
|
||||
error = ['Socket not running!']
|
||||
force_error = True
|
||||
|
||||
# start_time = time.time()
|
||||
|
||||
# Process the packet
|
||||
if data_raw:
|
||||
# print('SIZE:', len(data_raw))
|
||||
# print('DATA:', data_raw)
|
||||
|
||||
# Process animation data
|
||||
error, force_error = self.process_data(data_raw)
|
||||
|
||||
# print(round((time.time() - start_time) * 1000, 4), 'ms')
|
||||
|
||||
self.handle_ui_updates(received)
|
||||
self.handle_error(error, force_error)
|
||||
|
||||
def process_data(self, data_raw) -> ([str], bool):
|
||||
"""
|
||||
Processes the received data. If there was an error it returns a list of strings creating the error message
|
||||
and if the error should be forced to show immediately instead of after a couple of packages
|
||||
:param data_raw:
|
||||
:return:
|
||||
"""
|
||||
try:
|
||||
animations.live_data.init(data_raw)
|
||||
except ValueError as e:
|
||||
print('Packet contained no data')
|
||||
print(e)
|
||||
return ['Packets contain no data!'], False
|
||||
except (UnicodeDecodeError, TypeError) as e:
|
||||
print('Wrong live data format! Use JSON v2 or higher!')
|
||||
print(e)
|
||||
print(traceback.format_exc())
|
||||
return ['Wrong data format!', 'Use JSON v2 or higher!'], True
|
||||
except KeyError as e:
|
||||
print('KeyError:', e)
|
||||
return ['Incompatible JSON version!', 'Use the latest Studio', 'and plugin versions.'], True
|
||||
except ImportError as e:
|
||||
# This error occurs specifically when LZ4 isn't supported by the operating system
|
||||
if "os" in e.msg:
|
||||
print('LZ4 unsupported by OS!', 'Use "Json" in the', 'Custom panel in Studio.')
|
||||
return ['LZ4 unsupported by OS!', 'Use "Json" in the', 'Custom panel in Studio.'], True
|
||||
|
||||
# This error occurs, when the LZ4 package could not be loaded while it was needed
|
||||
print('LZ4 unsupported by OS or', 'Blender! Use "Json" in the', 'Custom panel in Studio.')
|
||||
return ['LZ4 unsupported by OS or', 'Blender! Use "Json" in the', 'Custom panel in Studio.'], True
|
||||
|
||||
animations.animate()
|
||||
|
||||
return None, False
|
||||
|
||||
def handle_ui_updates(self, received):
|
||||
# Update UI every 5 seconds when packets are received continuously
|
||||
if received:
|
||||
self.i += 1
|
||||
self.i_np = 0
|
||||
if self.i % (bpy.context.scene.rsl_receiver_fps * 5) == 0:
|
||||
utils.ui_refresh_properties()
|
||||
utils.ui_refresh_view_3d()
|
||||
return
|
||||
|
||||
# If receiving a packet after one second of no packets, update UI with next packet
|
||||
self.i_np += 1
|
||||
if self.i_np == bpy.context.scene.rsl_receiver_fps:
|
||||
self.i = -1
|
||||
|
||||
def handle_error(self, error, force_error):
|
||||
global show_error
|
||||
if not error:
|
||||
self.error_count = 0
|
||||
if not show_error:
|
||||
return
|
||||
self.error_temp = []
|
||||
show_error = []
|
||||
utils.ui_refresh_view_3d()
|
||||
print('REFRESH')
|
||||
return
|
||||
|
||||
if not self.error_temp:
|
||||
self.error_temp = error
|
||||
if force_error:
|
||||
self.error_count = bpy.context.scene.rsl_receiver_fps - 1
|
||||
return
|
||||
|
||||
if error == self.error_temp:
|
||||
self.error_count += 1
|
||||
else:
|
||||
self.error_temp = error
|
||||
if force_error:
|
||||
self.error_count = bpy.context.scene.rsl_receiver_fps
|
||||
else:
|
||||
self.error_count = 0
|
||||
|
||||
if self.error_count == bpy.context.scene.rsl_receiver_fps:
|
||||
show_error = self.error_temp
|
||||
utils.ui_refresh_view_3d()
|
||||
print('REFRESH')
|
||||
|
||||
def start(self, port):
|
||||
self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
self.sock.setblocking(False)
|
||||
self.sock.bind(('', port))
|
||||
|
||||
self.i = -1
|
||||
self.i_np = 0
|
||||
|
||||
self.error_temp = []
|
||||
self.error_count = 0
|
||||
|
||||
global show_error
|
||||
show_error = False
|
||||
|
||||
print("Rokoko Studio Live started listening on port " + str(port))
|
||||
|
||||
def stop(self):
|
||||
self.sock.close()
|
||||
print("Rokoko Studio Live stopped listening")
|
||||
@@ -0,0 +1,323 @@
|
||||
import bpy
|
||||
import copy
|
||||
from math import radians, degrees
|
||||
|
||||
from collections import OrderedDict
|
||||
|
||||
recorded_data = {}
|
||||
recorded_timestamps = OrderedDict()
|
||||
|
||||
|
||||
def toggle_recording(self, context):
|
||||
new_state = context.scene.rsl_recording
|
||||
|
||||
if new_state:
|
||||
start_recorder(context)
|
||||
else:
|
||||
stop_recorder(context)
|
||||
|
||||
|
||||
def start_recorder(context):
|
||||
if recorded_data:
|
||||
return
|
||||
# Here can be stuff done when starting the recorder
|
||||
pass
|
||||
|
||||
|
||||
def stop_recorder(context):
|
||||
if not recorded_data:
|
||||
return
|
||||
|
||||
# Set animation settings
|
||||
context.scene.render.fps = context.scene.rsl_receiver_fps
|
||||
|
||||
# Convert timestamps to keyframes to have a shared time axis
|
||||
convert_timestamps_to_keyframes()
|
||||
|
||||
# Process each type of recorded data
|
||||
for data_type, objects in recorded_data.items():
|
||||
if not objects:
|
||||
continue
|
||||
|
||||
if data_type == 'actors':
|
||||
for obj_name, data in objects.items():
|
||||
process_actor_recording(obj_name, data)
|
||||
|
||||
elif data_type == 'faces':
|
||||
for obj_name, data in objects.items():
|
||||
process_face_recording(obj_name, data)
|
||||
|
||||
elif data_type == 'objects':
|
||||
for obj_name, data in objects.items():
|
||||
process_object_recording(obj_name, data)
|
||||
|
||||
# Clear recorded data
|
||||
recorded_data.clear()
|
||||
recorded_timestamps.clear()
|
||||
|
||||
print('\nSuccessfully saved the recording!')
|
||||
|
||||
|
||||
def process_actor_recording(obj_name, data):
|
||||
armature = bpy.data.objects.get(obj_name)
|
||||
if not armature:
|
||||
print('Armature', obj_name, 'not found!')
|
||||
return
|
||||
|
||||
# Create new action
|
||||
action = bpy.data.actions.new(name='Anim Arm ' + obj_name)
|
||||
action.use_fake_user = True
|
||||
armature.animation_data_create().action = action
|
||||
|
||||
# Handle recorded data
|
||||
data_paths = OrderedDict()
|
||||
prev_rotations = {}
|
||||
rotation_modifiers = {}
|
||||
for item in data:
|
||||
bone_name = item["bone_name"]
|
||||
|
||||
if item['location']:
|
||||
data_path = 'pose.bones["%s"].location' % bone_name
|
||||
if not data_paths.get(data_path):
|
||||
data_paths[data_path] = []
|
||||
data_paths[data_path].append((item['timestamp'], item['location']))
|
||||
|
||||
if item['rotation']:
|
||||
rotation = item['rotation']
|
||||
data_path = 'pose.bones["%s"].rotation_euler' % bone_name
|
||||
if not data_paths.get(data_path):
|
||||
data_paths[data_path] = []
|
||||
|
||||
# Load previous rotation from dict
|
||||
prev_rot = prev_rotations.get(bone_name)
|
||||
if not prev_rot:
|
||||
prev_rot = rotation
|
||||
|
||||
# Fix each rotation axis separately
|
||||
for i in [0, 1, 2]:
|
||||
# Load rotation modifier of the bone rotation axis
|
||||
# The rotation modifier is used to cut down on processing time. It gets set when a axis had been normalized
|
||||
# and then is used to apply the same fix to subsequent rotations. This prevents having to normalize subsequent rotations
|
||||
rotation_mod = rotation_modifiers.get((bone_name, i))
|
||||
if not rotation_mod:
|
||||
rotation_mod = 0
|
||||
|
||||
# Get axis rotation in degrees, since they are stored as radians. Also add the rotation modifier to the rotation
|
||||
axis = degrees(rotation[i]) + rotation_mod
|
||||
axis_prev = degrees(prev_rot[i])
|
||||
|
||||
# Normalize the rotation
|
||||
axis_normalized, rotation_mod_new = normalize_rotation(axis, axis_prev)
|
||||
|
||||
# Save the normalized axis to the rotation and save the rotation modifier
|
||||
rotation[i] = radians(axis_normalized)
|
||||
rotation_modifiers[bone_name, i] = rotation_mod + rotation_mod_new
|
||||
|
||||
# Save the rotation to the data paths and save the current rotation as the previous rotation
|
||||
data_paths[data_path].append((item['timestamp'], rotation))
|
||||
prev_rotations[bone_name] = rotation
|
||||
|
||||
use_inherit_rotation = False
|
||||
data_path = 'data.bones["%s"].use_inherit_rotation' % bone_name
|
||||
if not data_paths.get(data_path):
|
||||
data_paths[data_path] = []
|
||||
data_paths[data_path].append((item['timestamp'], [use_inherit_rotation]))
|
||||
|
||||
# Go through each datapath (fcurve) and add all keyframes at once
|
||||
for data_path, values_tmp in data_paths.items():
|
||||
frame_count = len(values_tmp)
|
||||
values_tmp = list(zip(*values_tmp)) # This unzips the list of tuples into two separate lists
|
||||
timestamps = list(values_tmp[0])
|
||||
values = list(values_tmp[1])
|
||||
index_len = len(values[0])
|
||||
|
||||
for axis_i in range(index_len):
|
||||
curve = action.fcurves.new(data_path=data_path, index=axis_i)
|
||||
keyframe_points = curve.keyframe_points
|
||||
keyframe_points.add(frame_count)
|
||||
|
||||
for frame_i in range(frame_count):
|
||||
timestamp = timestamps[frame_i]
|
||||
transform = values[frame_i][axis_i]
|
||||
keyframe_points[frame_i].co = (
|
||||
recorded_timestamps[timestamp],
|
||||
transform)
|
||||
keyframe_points[frame_i].interpolation = 'LINEAR'
|
||||
|
||||
|
||||
def process_object_recording(obj_name, data):
|
||||
obj = bpy.data.objects.get(obj_name)
|
||||
if not obj:
|
||||
print('Object', obj_name, 'not found!')
|
||||
return
|
||||
|
||||
# Create new action
|
||||
action = bpy.data.actions.new(name='Anim Obj ' + obj_name)
|
||||
action.use_fake_user = True
|
||||
obj.animation_data_create().action = action
|
||||
|
||||
# Handle recording data
|
||||
data_paths = OrderedDict()
|
||||
for item in data:
|
||||
if item['location']:
|
||||
data_path = 'location'
|
||||
if not data_paths.get(data_path):
|
||||
data_paths[data_path] = []
|
||||
|
||||
data_paths[data_path].append((item['timestamp'], item['location']))
|
||||
|
||||
if item['rotation']:
|
||||
data_path = 'rotation_quaternion'
|
||||
if not data_paths.get(data_path):
|
||||
data_paths[data_path] = []
|
||||
|
||||
data_paths[data_path].append((item['timestamp'], item['rotation']))
|
||||
|
||||
for data_path, values in data_paths.items():
|
||||
# print(data_path)
|
||||
frame_count = len(values)
|
||||
index_len = 3 if data_path.endswith('location') else 4
|
||||
|
||||
for axis_i in range(index_len):
|
||||
curve = action.fcurves.new(data_path=data_path, index=axis_i)
|
||||
keyframe_points = curve.keyframe_points
|
||||
keyframe_points.add(frame_count)
|
||||
|
||||
for frame_i in range(frame_count):
|
||||
timestamp = values[frame_i][0]
|
||||
transform = values[frame_i][1]
|
||||
keyframe_points[frame_i].co = (
|
||||
recorded_timestamps[timestamp],
|
||||
transform[axis_i])
|
||||
keyframe_points[frame_i].interpolation = 'LINEAR'
|
||||
|
||||
|
||||
def process_face_recording(obj_name, data):
|
||||
mesh = bpy.data.objects.get(obj_name)
|
||||
if not mesh:
|
||||
print('Object', obj_name, 'not found!')
|
||||
return
|
||||
|
||||
# Create new action
|
||||
action = bpy.data.actions.new(name='Anim Face ' + obj_name)
|
||||
action.use_fake_user = True
|
||||
mesh.animation_data_create().action = action
|
||||
|
||||
# Handle recording data
|
||||
data_paths = OrderedDict()
|
||||
for item in data:
|
||||
data_path = 'data.shape_keys.key_blocks["%s"].value' % item['shapekey_name']
|
||||
if not data_paths.get(data_path):
|
||||
data_paths[data_path] = []
|
||||
|
||||
data_paths[data_path].append((item['timestamp'], item['value']))
|
||||
|
||||
for data_path, values in data_paths.items():
|
||||
# print(data_path)
|
||||
frame_count = len(values)
|
||||
|
||||
curve = action.fcurves.new(data_path=data_path, index=0)
|
||||
keyframe_points = curve.keyframe_points
|
||||
keyframe_points.add(frame_count)
|
||||
|
||||
for frame_i in range(frame_count):
|
||||
timestamp = values[frame_i][0]
|
||||
shapekey_value = values[frame_i][1]
|
||||
keyframe_points[frame_i].co = (
|
||||
recorded_timestamps[timestamp],
|
||||
shapekey_value)
|
||||
keyframe_points[frame_i].interpolation = 'LINEAR'
|
||||
|
||||
|
||||
def normalize_rotation(axis, axis_prev):
|
||||
rotation_mod = 0
|
||||
if abs(axis - axis_prev) > 180:
|
||||
desired_axis = axis
|
||||
if axis_prev > axis:
|
||||
while abs(desired_axis - axis_prev) > 180 and axis_prev > axis:
|
||||
print(axis_prev, axis, desired_axis)
|
||||
desired_axis += 360
|
||||
rotation_mod += 360
|
||||
axis = desired_axis
|
||||
else:
|
||||
while abs(desired_axis - axis_prev) > 180 and axis_prev < axis:
|
||||
print(axis_prev, axis, desired_axis)
|
||||
desired_axis -= 360
|
||||
rotation_mod -= 360
|
||||
axis = desired_axis
|
||||
return axis, rotation_mod
|
||||
|
||||
|
||||
def convert_timestamps_to_keyframes():
|
||||
timestamps = list(recorded_timestamps.keys())
|
||||
|
||||
def get_frame(frame_number):
|
||||
return int(round((timestamps[frame_number] - timestamps[0]) * bpy.context.scene.rsl_receiver_fps, 0))
|
||||
|
||||
# Fix frame numbers that are incorrect because of rounding errors
|
||||
for i, timestamp in enumerate(timestamps):
|
||||
curr_frame = get_frame(i)
|
||||
|
||||
if 0 < i < len(timestamps) - 1:
|
||||
prev_frame = get_frame(i - 1)
|
||||
next_frame = get_frame(i + 1)
|
||||
if prev_frame == curr_frame and next == curr_frame + 2:
|
||||
curr_frame += 1
|
||||
if next_frame == curr_frame and prev_frame == curr_frame - 2:
|
||||
curr_frame -= 1
|
||||
|
||||
if i == len(timestamps) - 1:
|
||||
prev_frame = get_frame(i - 1)
|
||||
if prev_frame == curr_frame:
|
||||
curr_frame += 1
|
||||
|
||||
recorded_timestamps[timestamp] = curr_frame
|
||||
|
||||
|
||||
def record_bone(timestamp, arm_name, bone_name, rotation, location=None):
|
||||
if not recorded_data.get('actors'):
|
||||
recorded_data['actors'] = {}
|
||||
if not recorded_data['actors'].get(arm_name):
|
||||
recorded_data['actors'][arm_name] = []
|
||||
|
||||
data = {
|
||||
'timestamp': timestamp,
|
||||
'bone_name': bone_name,
|
||||
'rotation': copy.deepcopy(rotation),
|
||||
'location': copy.deepcopy(location)
|
||||
}
|
||||
|
||||
recorded_data['actors'][arm_name].append(data)
|
||||
recorded_timestamps[timestamp] = 0
|
||||
|
||||
|
||||
def record_face(timestamp, mesh_name, shapekey_name, value):
|
||||
if not recorded_data.get('faces'):
|
||||
recorded_data['faces'] = {}
|
||||
if not recorded_data['faces'].get(mesh_name):
|
||||
recorded_data['faces'][mesh_name] = []
|
||||
|
||||
data = {
|
||||
'timestamp': timestamp,
|
||||
'shapekey_name': shapekey_name,
|
||||
'value': copy.deepcopy(value)
|
||||
}
|
||||
|
||||
recorded_data['faces'][mesh_name].append(data)
|
||||
recorded_timestamps[timestamp] = 0
|
||||
|
||||
|
||||
def record_object(timestamp, obj_name, rotation, location):
|
||||
if not recorded_data.get('objects'):
|
||||
recorded_data['objects'] = {}
|
||||
if not recorded_data['objects'].get(obj_name):
|
||||
recorded_data['objects'][obj_name] = []
|
||||
|
||||
data = {
|
||||
'timestamp': timestamp,
|
||||
'rotation': copy.deepcopy(rotation),
|
||||
'location': copy.deepcopy(location)
|
||||
}
|
||||
|
||||
recorded_data['objects'][obj_name].append(data)
|
||||
recorded_timestamps[timestamp] = 0
|
||||
@@ -0,0 +1,23 @@
|
||||
import bpy
|
||||
|
||||
|
||||
# This filters the objects shown to only include armatures and under certain conditions
|
||||
def poll_source_armatures(self, obj):
|
||||
return obj.type == 'ARMATURE' and obj.animation_data and obj.animation_data.action
|
||||
|
||||
|
||||
def poll_target_armatures(self, obj):
|
||||
return obj.type == 'ARMATURE' and obj != get_source_armature()
|
||||
|
||||
|
||||
# If the retargeting armatures get changed, clear the bone list
|
||||
def clear_bone_list(self, context):
|
||||
context.scene.rsl_retargeting_bone_list.clear()
|
||||
|
||||
|
||||
def get_source_armature():
|
||||
return bpy.context.scene.rsl_retargeting_armature_source
|
||||
|
||||
|
||||
def get_target_armature():
|
||||
return bpy.context.scene.rsl_retargeting_armature_target
|
||||
@@ -0,0 +1,293 @@
|
||||
import bpy
|
||||
import copy
|
||||
|
||||
from . import utils
|
||||
from ..operators import receiver
|
||||
|
||||
objects = {}
|
||||
faces = {}
|
||||
armatures = {}
|
||||
|
||||
hidden_meshes = {}
|
||||
|
||||
|
||||
def save_scene():
|
||||
for obj in bpy.context.scene.objects:
|
||||
save_object(obj)
|
||||
if obj.type == 'MESH':
|
||||
save_face(obj)
|
||||
elif obj.type == 'ARMATURE':
|
||||
save_armature(obj)
|
||||
|
||||
|
||||
def load_scene():
|
||||
for obj in bpy.context.scene.objects:
|
||||
load_object(obj)
|
||||
if obj.type == 'MESH':
|
||||
load_face(obj)
|
||||
elif obj.type == 'ARMATURE':
|
||||
load_armature(obj)
|
||||
|
||||
hidden_meshes.clear()
|
||||
|
||||
|
||||
# Object handler
|
||||
def save_object(obj):
|
||||
if obj.rsl_animations_props_trackers == 'None':
|
||||
return
|
||||
|
||||
global objects
|
||||
rotation_mode = obj.rotation_mode
|
||||
obj.rotation_mode = 'QUATERNION'
|
||||
objects[obj.name] = copy.deepcopy({
|
||||
'location': obj.location,
|
||||
'rotation': obj.rotation_quaternion,
|
||||
'rotation_mode': rotation_mode,
|
||||
# 'hidden': obj.hide_get()
|
||||
})
|
||||
|
||||
|
||||
def load_object(obj):
|
||||
if not bpy.context.scene.rsl_reset_scene_on_stop:
|
||||
return
|
||||
|
||||
global objects
|
||||
|
||||
obj_data = objects.get(obj.name)
|
||||
if not obj_data:
|
||||
return
|
||||
|
||||
obj.rotation_mode = 'QUATERNION'
|
||||
obj.location = obj_data['location']
|
||||
obj.rotation_quaternion = obj_data['rotation']
|
||||
# obj.rotation_mode = obj_data['rotation_mode']
|
||||
# obj.hide_set(obj_data['hidden'])
|
||||
|
||||
# Remove element from dictionary
|
||||
objects.pop(obj.name)
|
||||
|
||||
|
||||
# Face mesh handler
|
||||
def save_face(obj):
|
||||
if not hasattr(obj.data, 'shape_keys') or not hasattr(obj.data.shape_keys, 'key_blocks'):
|
||||
return
|
||||
if obj.rsl_animations_faces == 'None':
|
||||
return
|
||||
|
||||
global faces
|
||||
shapekeys = {}
|
||||
|
||||
for shapekey in obj.data.shape_keys.key_blocks:
|
||||
shapekeys[shapekey.name] = shapekey.value
|
||||
|
||||
faces[obj.name] = copy.deepcopy(shapekeys)
|
||||
|
||||
if obj.name in hidden_meshes.keys():
|
||||
unhide_mesh(obj, hidden_meshes[obj.name])
|
||||
|
||||
|
||||
def load_face(obj):
|
||||
if not bpy.context.scene.rsl_reset_scene_on_stop:
|
||||
return
|
||||
|
||||
global faces
|
||||
shapekey_data = faces.get(obj.name)
|
||||
if not shapekey_data or not hasattr(obj.data, 'shape_keys') or not hasattr(obj.data.shape_keys, 'key_blocks'):
|
||||
return
|
||||
|
||||
for shapekey_name, value in shapekey_data.items():
|
||||
shapekey = obj.data.shape_keys.key_blocks.get(shapekey_name)
|
||||
if not shapekey:
|
||||
continue
|
||||
|
||||
shapekey.value = value
|
||||
|
||||
# Remove element from dictionary
|
||||
faces.pop(obj.name)
|
||||
|
||||
# Hide this mesh if it is animated by an armature and it should be hidden
|
||||
if bpy.context.scene.rsl_hide_mesh_during_play:
|
||||
for mod in obj.modifiers:
|
||||
if mod.type == 'ARMATURE':
|
||||
armature = mod.object
|
||||
if armatures.get(armature.name):
|
||||
hide_mesh(obj, armature)
|
||||
|
||||
|
||||
# Armature handler
|
||||
def save_armature(obj):
|
||||
global armatures
|
||||
|
||||
# Return if no actor and no glove is assigned to this armature
|
||||
# if not obj.rsl_animations_actors or obj.rsl_animations_actors == 'None': # <-- This should work but for some reason it doesn't
|
||||
if obj.rsl_animations_actors == 'None':
|
||||
print('NO ASSIGNED DATA:', obj.rsl_animations_actors)
|
||||
return
|
||||
|
||||
utils.set_active(obj)
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
|
||||
bones = {}
|
||||
|
||||
for bone in obj.pose.bones:
|
||||
# Fix rotation mode
|
||||
if bone.rotation_mode == 'QUATERNION':
|
||||
bone.rotation_mode = 'XYZ'
|
||||
|
||||
bones[bone.name] = {
|
||||
'location': bone.location,
|
||||
'rotation': bone.rotation_euler,
|
||||
'rotation_mode': bone.rotation_mode,
|
||||
'inherit_rotation': obj.data.bones.get(bone.name).use_inherit_rotation
|
||||
}
|
||||
|
||||
armatures[obj.name] = copy.deepcopy(bones)
|
||||
|
||||
hide_meshes_on_play(obj)
|
||||
|
||||
|
||||
def load_armature(obj):
|
||||
unhide_meshes_on_stop(obj)
|
||||
|
||||
if not bpy.context.scene.rsl_reset_scene_on_stop:
|
||||
return
|
||||
|
||||
global armatures
|
||||
bone_data = armatures.get(obj.name)
|
||||
if not bone_data:
|
||||
return
|
||||
|
||||
for bone_name, bone_data in bone_data.items():
|
||||
bone = obj.pose.bones.get(bone_name)
|
||||
if not bone:
|
||||
continue
|
||||
|
||||
location = bone_data['location']
|
||||
rotation = bone_data['rotation']
|
||||
rotation_mode = bone_data['rotation_mode']
|
||||
inherit_rotation = bone_data['inherit_rotation']
|
||||
|
||||
# Fix rotation mode
|
||||
if rotation_mode == 'QUATERNION':
|
||||
rotation_mode = 'XYZ'
|
||||
|
||||
bone.location = location
|
||||
bone.rotation_mode = rotation_mode
|
||||
bone.rotation_euler = rotation
|
||||
obj.data.bones.get(bone_name).use_inherit_rotation = inherit_rotation
|
||||
|
||||
# Remove element from dictionary
|
||||
armatures.pop(obj.name)
|
||||
|
||||
|
||||
def update_object(self, context):
|
||||
if not receiver.receiver_enabled:
|
||||
return
|
||||
|
||||
obj = context.object
|
||||
new_state = obj.rsl_animations_props_trackers
|
||||
|
||||
if new_state != 'None':
|
||||
if not objects.get(obj.name):
|
||||
save_object(obj)
|
||||
else:
|
||||
load_object(obj)
|
||||
|
||||
|
||||
def update_face(self, context):
|
||||
if not receiver.receiver_enabled:
|
||||
return
|
||||
|
||||
obj = context.object
|
||||
new_state = obj.rsl_animations_faces
|
||||
|
||||
if new_state != 'None':
|
||||
if not faces.get(obj.name):
|
||||
save_face(obj)
|
||||
else:
|
||||
load_face(obj)
|
||||
|
||||
|
||||
def update_actor(self, context):
|
||||
if not receiver.receiver_enabled:
|
||||
return
|
||||
|
||||
obj = context.object
|
||||
new_state = obj.rsl_animations_actors
|
||||
|
||||
if new_state != 'None':
|
||||
if not armatures.get(obj.name):
|
||||
save_armature(obj)
|
||||
else:
|
||||
load_armature(obj)
|
||||
|
||||
|
||||
def update_glove(self, context):
|
||||
if not receiver.receiver_enabled:
|
||||
return
|
||||
|
||||
obj = context.object
|
||||
new_state = obj.rsl_animations_gloves
|
||||
|
||||
if new_state != 'None':
|
||||
if not armatures.get(obj.name):
|
||||
save_armature(obj)
|
||||
else:
|
||||
load_armature(obj)
|
||||
|
||||
|
||||
def hide_meshes_on_play(armature):
|
||||
if not bpy.context.scene.rsl_hide_mesh_during_play:
|
||||
return
|
||||
|
||||
global faces
|
||||
for mesh in bpy.context.scene.objects:
|
||||
if mesh.type != 'MESH':
|
||||
continue
|
||||
|
||||
hide_mesh(mesh, armature)
|
||||
|
||||
|
||||
def unhide_meshes_on_stop(armature):
|
||||
for mesh_name in copy.copy(hidden_meshes).keys():
|
||||
mesh = bpy.context.scene.objects.get(mesh_name)
|
||||
if not mesh:
|
||||
continue
|
||||
unhide_mesh(mesh, armature)
|
||||
|
||||
|
||||
def update_hidden_meshes(self, context):
|
||||
if not receiver.receiver_enabled:
|
||||
return
|
||||
|
||||
new_state = context.scene.rsl_hide_mesh_during_play
|
||||
|
||||
for armature_name in armatures.keys():
|
||||
armature = bpy.context.scene.objects.get(armature_name)
|
||||
if not armature:
|
||||
continue
|
||||
|
||||
if new_state:
|
||||
hide_meshes_on_play(armature)
|
||||
else:
|
||||
unhide_meshes_on_stop(armature)
|
||||
|
||||
|
||||
def hide_mesh(mesh, armature):
|
||||
if faces.get(mesh.name):
|
||||
return
|
||||
|
||||
for mod in mesh.modifiers:
|
||||
if mod.type == 'ARMATURE' and mod.object == armature:
|
||||
mesh.hide_set(True)
|
||||
mod.object = None
|
||||
hidden_meshes[mesh.name] = armature
|
||||
|
||||
|
||||
def unhide_mesh(mesh, armature):
|
||||
mesh.hide_set(False)
|
||||
|
||||
for mod in mesh.modifiers:
|
||||
if mod.type == 'ARMATURE' and hidden_meshes[mesh.name] == armature:
|
||||
mod.object = hidden_meshes[mesh.name]
|
||||
hidden_meshes.pop(mesh.name)
|
||||
@@ -0,0 +1,85 @@
|
||||
import asyncio
|
||||
import sys
|
||||
import bpy
|
||||
import math
|
||||
from mathutils import Vector, Matrix
|
||||
from contextlib import suppress
|
||||
|
||||
|
||||
def ui_refresh_properties():
|
||||
# Refreshes the properties panel
|
||||
for windowManager in bpy.data.window_managers:
|
||||
for window in windowManager.windows:
|
||||
for area in window.screen.areas:
|
||||
if area.type == 'PROPERTIES':
|
||||
area.tag_redraw()
|
||||
|
||||
|
||||
def ui_refresh_view_3d():
|
||||
# Refreshes the view 3D panel
|
||||
for windowManager in bpy.data.window_managers:
|
||||
for window in windowManager.windows:
|
||||
for area in window.screen.areas:
|
||||
if area.type == 'VIEW_3D':
|
||||
area.tag_redraw()
|
||||
|
||||
|
||||
def ui_refresh_all():
|
||||
if not hasattr(bpy.data, "window_managers"):
|
||||
return
|
||||
# Refreshes all panels
|
||||
for windowManager in bpy.data.window_managers:
|
||||
for window in windowManager.windows:
|
||||
for area in window.screen.areas:
|
||||
area.tag_redraw()
|
||||
|
||||
|
||||
def reprint(*x):
|
||||
# This prints a message in the same console line continuously
|
||||
sys.stdout.write("\r" + " ".join(x))
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
def set_active(obj):
|
||||
obj.select_set(True)
|
||||
obj.hide_set(False)
|
||||
bpy.context.view_layer.objects.active = obj
|
||||
|
||||
|
||||
def mat3_to_vec_roll(mat):
|
||||
vecmat = vec_roll_to_mat3(mat.col[1], 0)
|
||||
vecmatinv = vecmat.inverted()
|
||||
rollmat = vecmatinv @ mat
|
||||
roll = math.atan2(rollmat[0][2], rollmat[2][2])
|
||||
return roll
|
||||
|
||||
|
||||
def vec_roll_to_mat3(vec, roll):
|
||||
target = Vector((0, 0.1, 0))
|
||||
nor = vec.normalized()
|
||||
axis = target.cross(nor)
|
||||
if axis.dot(axis) > 0.0000000001:
|
||||
axis.normalize()
|
||||
theta = target.angle(nor)
|
||||
bMatrix = Matrix.Rotation(theta, 3, axis)
|
||||
else:
|
||||
updown = 1 if target.dot(nor) > 0 else -1
|
||||
bMatrix = Matrix.Scale(updown, 3)
|
||||
bMatrix[2][2] = 1.0
|
||||
|
||||
rMatrix = Matrix.Rotation(roll, 3, nor)
|
||||
mat = rMatrix @ bMatrix
|
||||
return mat
|
||||
|
||||
|
||||
async def cancel_gen(agen):
|
||||
"""
|
||||
Stops an asynchronous generator from outside.
|
||||
:param agen: The asynchronous generator
|
||||
:return:
|
||||
"""
|
||||
task = asyncio.create_task(agen.__anext__())
|
||||
task.cancel()
|
||||
with suppress(Exception):
|
||||
await task
|
||||
await agen.aclose()
|
||||
@@ -0,0 +1,21 @@
|
||||
if "bpy" not in locals():
|
||||
import bpy
|
||||
from . import receiver
|
||||
from . import detector
|
||||
from . import recorder
|
||||
from . import actor
|
||||
from . import command_api
|
||||
from . import info
|
||||
from . import retargeting
|
||||
from . import login
|
||||
else:
|
||||
import importlib
|
||||
|
||||
importlib.reload(receiver)
|
||||
importlib.reload(detector)
|
||||
importlib.reload(recorder)
|
||||
importlib.reload(actor)
|
||||
importlib.reload(command_api)
|
||||
importlib.reload(info)
|
||||
importlib.reload(retargeting)
|
||||
importlib.reload(login)
|
||||
@@ -0,0 +1,134 @@
|
||||
import bpy
|
||||
import copy
|
||||
|
||||
from . import receiver
|
||||
|
||||
|
||||
class InitTPose(bpy.types.Operator):
|
||||
bl_idname = "rsl.init_tpose"
|
||||
bl_label = "Set as T-Pose"
|
||||
bl_description = "Press this button if you have this armature in T-Pose"
|
||||
bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}
|
||||
|
||||
def execute(self, context):
|
||||
obj = context.object
|
||||
if obj.type != 'ARMATURE':
|
||||
self.report({'ERROR'}, 'This is not an armature!')
|
||||
return {'CANCELLED'}
|
||||
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
|
||||
# Get current custom data
|
||||
custom_data = obj.get('CUSTOM')
|
||||
if not custom_data:
|
||||
custom_data = {}
|
||||
|
||||
bones = {}
|
||||
|
||||
# Save local and global space rotations and local and object space locations for each bone
|
||||
for bone in obj.pose.bones:
|
||||
# Save rotation mode
|
||||
rotation_mode = bone.rotation_mode
|
||||
|
||||
# Save bone pose data
|
||||
bone.rotation_mode = 'QUATERNION'
|
||||
bones[bone.name] = {
|
||||
'location_local': bone.location,
|
||||
'location_object': bone.matrix @ bone.location,
|
||||
'rotation_local': bone.rotation_quaternion,
|
||||
'rotation_global': bone.matrix.to_quaternion(),
|
||||
'inherit_rotation': obj.data.bones.get(bone.name).use_inherit_rotation,
|
||||
}
|
||||
|
||||
# Load rotation mode
|
||||
bone.rotation_mode = rotation_mode
|
||||
|
||||
# Save tpose data to custom data
|
||||
custom_data['rsl_tpose_bones'] = copy.deepcopy(bones)
|
||||
obj['CUSTOM'] = custom_data
|
||||
|
||||
self.report({'INFO'}, 'T-Pose successfully saved!')
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class ResetTPose(bpy.types.Operator):
|
||||
bl_idname = "rsl.reset_tpose"
|
||||
bl_label = "Reset to T-Pose"
|
||||
bl_description = "Use this to reset the armature to it's T-Pose"
|
||||
bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}
|
||||
|
||||
def execute(self, context):
|
||||
if receiver.receiver_enabled:
|
||||
self.report({'ERROR'}, 'Receiver is currently running. Please stop it first.')
|
||||
return {'CANCELLED'}
|
||||
|
||||
obj = context.object
|
||||
if obj.type != 'ARMATURE':
|
||||
self.report({'ERROR'}, 'This is not an armature!')
|
||||
return {'CANCELLED'}
|
||||
|
||||
# Get current custom data
|
||||
custom_data = obj.get('CUSTOM')
|
||||
if not custom_data:
|
||||
self.report({'ERROR'}, 'Please set the T-Pose first.')
|
||||
return {'CANCELLED'}
|
||||
|
||||
# Get tpose data from custom data
|
||||
tpose_bones = custom_data.get('rsl_tpose_bones')
|
||||
if not tpose_bones:
|
||||
self.report({'ERROR'}, 'Please set the T-Pose first.')
|
||||
return {'CANCELLED'}
|
||||
|
||||
# Apply locations and rotations to bones
|
||||
for bone_name, data in tpose_bones.items():
|
||||
bone = obj.pose.bones.get(bone_name)
|
||||
if bone:
|
||||
# Save rotation mode
|
||||
rotation_mode = bone.rotation_mode
|
||||
if rotation_mode == 'QUATERNION':
|
||||
rotation_mode = 'XYZ'
|
||||
|
||||
bone.rotation_mode = 'QUATERNION'
|
||||
bone.rotation_quaternion = data['rotation_local']
|
||||
bone.location = data['location_local']
|
||||
obj.data.bones.get(bone_name).use_inherit_rotation = data['inherit_rotation']
|
||||
|
||||
# Load rotation mode
|
||||
bone.rotation_mode = rotation_mode
|
||||
|
||||
self.report({'INFO'}, 'T-Pose successfully restored!')
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class PrintCurrentPose(bpy.types.Operator):
|
||||
bl_idname = "rsl.print_current_pose"
|
||||
bl_label = "Print"
|
||||
bl_description = "Debugging. Prints world rotation of armature bones"
|
||||
bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}
|
||||
|
||||
def execute(self, context):
|
||||
obj = context.object
|
||||
if obj.type != 'ARMATURE':
|
||||
self.report({'ERROR'}, 'This is not an armature!')
|
||||
return {'CANCELLED'}
|
||||
|
||||
for bone in obj.pose.bones:
|
||||
# Save rotation mode
|
||||
rotation_mode = bone.rotation_mode
|
||||
if rotation_mode == 'QUATERNION':
|
||||
rotation_mode = 'XYZ'
|
||||
|
||||
bone.rotation_mode = 'QUATERNION'
|
||||
rot = bone.matrix.to_euler().to_quaternion().copy()
|
||||
i = 5
|
||||
print('actor_bones[\'' + bone.name + '\'] = Quaternion(('
|
||||
+ str(round(rot[0], i)) + ', '
|
||||
+ str(round(rot[1], i)) + ', '
|
||||
+ str(round(rot[2], i)) + ', '
|
||||
+ str(round(rot[3], i))
|
||||
+ '))')
|
||||
|
||||
# Load rotation mode
|
||||
bone.rotation_mode = rotation_mode
|
||||
|
||||
return {'FINISHED'}
|
||||
@@ -0,0 +1,146 @@
|
||||
|
||||
import bpy
|
||||
import requests
|
||||
import traceback
|
||||
|
||||
|
||||
class CommandTest(bpy.types.Operator):
|
||||
bl_idname = 'rsl.command_test'
|
||||
bl_label = 'Test Command API'
|
||||
bl_description = 'Testing'
|
||||
bl_options = {'INTERNAL'}
|
||||
|
||||
def execute(self, context):
|
||||
try:
|
||||
request = get_request('')
|
||||
except requests.exceptions.ConnectionError:
|
||||
self.report({'ERROR'}, 'Could not connect to Rokoko Studio!')
|
||||
return {'CANCELLED'}
|
||||
|
||||
data = request.json()
|
||||
if is_error(self, data):
|
||||
return {'CANCELLED'}
|
||||
|
||||
self.report({'INFO'}, 'Successfully tested!')
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class StartCalibration(bpy.types.Operator):
|
||||
bl_idname = 'rsl.command_start_calibration'
|
||||
bl_label = 'Start Calibration'
|
||||
bl_description = 'Starts calibration of a Smartsuit Pro'
|
||||
bl_options = {'INTERNAL'}
|
||||
|
||||
def execute(self, context):
|
||||
try:
|
||||
request = post_request('/calibrate')
|
||||
except requests.exceptions.ConnectionError:
|
||||
print(traceback.format_exc())
|
||||
self.report({'ERROR'}, 'Could not connect to Rokoko Studio!')
|
||||
return {'CANCELLED'}
|
||||
|
||||
data = request.json()
|
||||
if is_error(self, data):
|
||||
return {'CANCELLED'}
|
||||
|
||||
self.report({'INFO'}, 'Calibration started successfully!')
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class Restart(bpy.types.Operator):
|
||||
bl_idname = 'rsl.command_restart'
|
||||
bl_label = 'Restart Smartsuits'
|
||||
bl_description = 'Restarts all Smartsuit Pro\'s'
|
||||
bl_options = {'INTERNAL'}
|
||||
|
||||
def execute(self, context):
|
||||
try:
|
||||
request = post_request('/restart')
|
||||
except requests.exceptions.ConnectionError:
|
||||
print(traceback.format_exc())
|
||||
self.report({'ERROR'}, 'Could not connect to Rokoko Studio!')
|
||||
return {'CANCELLED'}
|
||||
|
||||
data = request.json()
|
||||
if is_error(self, data):
|
||||
return {'CANCELLED'}
|
||||
|
||||
self.report({'INFO'}, 'Smartsuits restarted successfully!!')
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class StartRecording(bpy.types.Operator):
|
||||
bl_idname = 'rsl.command_start_recording'
|
||||
bl_label = 'Start Recording'
|
||||
bl_description = 'Starts recording all connected Smartsuit Pro\'s'
|
||||
bl_options = {'INTERNAL'}
|
||||
|
||||
def execute(self, context):
|
||||
try:
|
||||
request = post_request('/recording/start')
|
||||
except requests.exceptions.ConnectionError:
|
||||
print(traceback.format_exc())
|
||||
self.report({'ERROR'}, 'Could not connect to Rokoko Studio!')
|
||||
return {'CANCELLED'}
|
||||
|
||||
data = request.json()
|
||||
if is_error(self, data):
|
||||
return {'CANCELLED'}
|
||||
|
||||
self.report({'INFO'}, 'Recording started successfully!')
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class StopRecording(bpy.types.Operator):
|
||||
bl_idname = 'rsl.command_stop_recording'
|
||||
bl_label = 'Stop Recording'
|
||||
bl_description = 'Stops recording all connected Smartsuit Pro\'s'
|
||||
bl_options = {'INTERNAL'}
|
||||
|
||||
def execute(self, context):
|
||||
try:
|
||||
request = post_request('/recording/stop')
|
||||
except requests.exceptions.ConnectionError:
|
||||
print(traceback.format_exc())
|
||||
self.report({'ERROR'}, 'Could not connect to Rokoko Studio!')
|
||||
return {'CANCELLED'}
|
||||
|
||||
data = request.json()
|
||||
if is_error(self, data):
|
||||
return {'CANCELLED'}
|
||||
|
||||
self.report({'INFO'}, 'Recording stopped successfully!')
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
def get_request(additions):
|
||||
scn = bpy.context.scene
|
||||
return requests.get(f'http://{scn.rsl_command_ip_address}:{scn.rsl_command_ip_port}/v1/{scn.rsl_command_api_key}' + additions)
|
||||
|
||||
|
||||
def post_request(additions, json=None):
|
||||
if json is None:
|
||||
json = {}
|
||||
scn = bpy.context.scene
|
||||
|
||||
url = f'http://{scn.rsl_command_ip_address}:{scn.rsl_command_ip_port}/v1/{scn.rsl_command_api_key}{additions}'
|
||||
print(url, json)
|
||||
request = requests.post(url, json=json)
|
||||
return request
|
||||
|
||||
|
||||
def is_error(self, data):
|
||||
print(data)
|
||||
if not data.get('response_code'):
|
||||
self.report({'ERROR'}, 'No response from Studio!')
|
||||
return True
|
||||
|
||||
if data.get('response_code') != 'OK':
|
||||
|
||||
if data.get('response_code') == 'INVALID_REQUEST':
|
||||
self.report({'ERROR'}, data.get('response_code') + '\n' + data.get('description') + ' Check your API key.')
|
||||
return True
|
||||
|
||||
self.report({'ERROR'}, data.get('response_code') + '\n' + data.get('description'))
|
||||
return True
|
||||
return False
|
||||
@@ -0,0 +1,227 @@
|
||||
import os
|
||||
import bpy
|
||||
import bpy_extras
|
||||
|
||||
from ..core import animation_lists
|
||||
from ..core import detection_manager
|
||||
from ..core import custom_schemes_manager
|
||||
|
||||
|
||||
class DetectFaceShapes(bpy.types.Operator):
|
||||
bl_idname = "rsl.detect_face_shapes"
|
||||
bl_label = "Auto Detect"
|
||||
bl_description = "Automatically detect face shape keys for supported naming schemes"
|
||||
bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}
|
||||
|
||||
def execute(self, context):
|
||||
obj = context.object
|
||||
|
||||
if not hasattr(obj.data, 'shape_keys') or not hasattr(obj.data.shape_keys, 'key_blocks'):
|
||||
self.report({'ERROR'}, 'This mesh has no shapekeys!')
|
||||
return {'CANCELLED'}
|
||||
|
||||
for shape_name_key in animation_lists.face_shapes:
|
||||
setattr(obj, 'rsl_face_' + shape_name_key, detection_manager.detect_shape(obj, shape_name_key))
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class DetectActorBones(bpy.types.Operator):
|
||||
bl_idname = "rsl.detect_actor_bones"
|
||||
bl_label = "Auto Detect"
|
||||
bl_description = "Automatically detect actor bones for supported naming schemes"
|
||||
bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}
|
||||
|
||||
def execute(self, context):
|
||||
obj = context.object
|
||||
|
||||
for bone_name_key in animation_lists.get_bones().keys():
|
||||
setattr(obj, 'rsl_actor_' + bone_name_key, detection_manager.detect_bone(obj, bone_name_key))
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class SaveCustomShapes(bpy.types.Operator):
|
||||
bl_idname = "rsl.save_custom_shapes"
|
||||
bl_label = "Save Custom Shapes"
|
||||
bl_description = "This saves the currently selected shapekeys and they will then get automatically detected"
|
||||
bl_options = {'INTERNAL'}
|
||||
|
||||
def execute(self, context):
|
||||
obj = context.object
|
||||
|
||||
# Go over all face shapekeys and see if the user changed the detected shapekey. If yes, save that new shapekey
|
||||
for shape_name_key in animation_lists.face_shapes:
|
||||
shape_name_selected = getattr(obj, 'rsl_face_' + shape_name_key)
|
||||
if not shape_name_selected:
|
||||
continue # TODO idea: maybe save these unselected choices as well
|
||||
|
||||
shape_name_detected = detection_manager.detect_shape(obj, shape_name_key)
|
||||
|
||||
if shape_name_detected == shape_name_selected: # This means that the user changed nothing, so don't save this
|
||||
continue
|
||||
|
||||
custom_schemes_manager.save_live_data_shape_to_list(shape_name_key, shape_name_selected, shape_name_detected)
|
||||
|
||||
# At the end save all custom shapes to the file
|
||||
custom_schemes_manager.save_to_file_and_update()
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class SaveCustomBones(bpy.types.Operator):
|
||||
bl_idname = "rsl.save_custom_bones"
|
||||
bl_label = "Save Custom Bones"
|
||||
bl_description = "This saves the currently selected bones and they will then get automatically detected"
|
||||
bl_options = {'INTERNAL'}
|
||||
|
||||
def execute(self, context):
|
||||
obj = context.object
|
||||
|
||||
# Go over all actor bones and see if the user changed the detected bone. If yes, save that new bone
|
||||
for bone_name_key in animation_lists.get_bones().keys():
|
||||
bone_name_selected = getattr(obj, 'rsl_actor_' + bone_name_key)
|
||||
if not bone_name_selected:
|
||||
continue # TODO idea: maybe save these unselected choices as well
|
||||
|
||||
bone_name_detected = detection_manager.detect_bone(obj, bone_name_key)
|
||||
|
||||
if bone_name_detected == bone_name_selected: # This means that the user changed nothing, so don't save this
|
||||
continue
|
||||
|
||||
custom_schemes_manager.save_live_data_bone_to_list(bone_name_key, bone_name_selected, bone_name_detected)
|
||||
|
||||
# At the end save all custom bones to the file
|
||||
custom_schemes_manager.save_to_file_and_update()
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class SaveCustomBonesRetargeting(bpy.types.Operator):
|
||||
bl_idname = "rsl.save_custom_bones_retargeting"
|
||||
bl_label = "Save Custom Bones"
|
||||
bl_description = "This saves the currently selected bones and they will then get automatically detected"
|
||||
bl_options = {'INTERNAL'}
|
||||
|
||||
def execute(self, context):
|
||||
# Save the bone list if the user changed anything
|
||||
custom_schemes_manager.save_retargeting_to_list()
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class ImportCustomBones(bpy.types.Operator, bpy_extras.io_utils.ImportHelper):
|
||||
bl_idname = "rsl.import_custom_schemes"
|
||||
bl_label = "Import Custom Scheme"
|
||||
bl_description = "Import a custom naming scheme"
|
||||
bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}
|
||||
|
||||
files: bpy.props.CollectionProperty(type=bpy.types.OperatorFileListElement, options={'HIDDEN', 'SKIP_SAVE'})
|
||||
directory: bpy.props.StringProperty(maxlen=1024, subtype='FILE_PATH', options={'HIDDEN', 'SKIP_SAVE'})
|
||||
filter_glob: bpy.props.StringProperty(default='*.json;', options={'HIDDEN'})
|
||||
|
||||
def execute(self, context):
|
||||
import_count = 0
|
||||
if self.directory:
|
||||
for f in self.files:
|
||||
file_name = f.name
|
||||
if not file_name.endswith('.json'):
|
||||
continue
|
||||
custom_schemes_manager.import_custom_list(self.directory, file_name)
|
||||
import_count += 1
|
||||
|
||||
# If this operator is called with no directory but a filepath argument, import that
|
||||
elif self.filepath:
|
||||
custom_schemes_manager.import_custom_list(os.path.dirname(self.filepath), os.path.basename(self.filepath))
|
||||
import_count += 1
|
||||
|
||||
custom_schemes_manager.save_to_file_and_update()
|
||||
|
||||
if not import_count:
|
||||
self.report({'ERROR'}, 'No files were imported.')
|
||||
return {'FINISHED'}
|
||||
|
||||
self.report({'INFO'}, 'Successfully imported new naming schemes.')
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class ExportCustomBones(bpy.types.Operator, bpy_extras.io_utils.ExportHelper):
|
||||
bl_idname = "rsl.export_custom_schemes"
|
||||
bl_label = "Export Custom Scheme"
|
||||
bl_description = "Export your custom naming schemes"
|
||||
bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}
|
||||
|
||||
filename_ext = ".json"
|
||||
filter_glob: bpy.props.StringProperty(default='*.json;', options={'HIDDEN'})
|
||||
|
||||
def execute(self, context):
|
||||
file_name = custom_schemes_manager.export_custom_list(self.filepath)
|
||||
|
||||
if not file_name:
|
||||
self.report({'ERROR'}, 'You don\'t have any custom naming schemes!')
|
||||
return {'FINISHED'}
|
||||
|
||||
self.report({'INFO'}, 'Exported custom naming schemes as "' + file_name + '".')
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class ClearCustomBones(bpy.types.Operator):
|
||||
bl_idname = "rsl.clear_custom_bones"
|
||||
bl_label = "Clear Custom Bones"
|
||||
bl_description = "Clear all custom bone naming schemes"
|
||||
bl_options = {'INTERNAL'}
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
|
||||
layout.separator()
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.scale_y = 0.5
|
||||
row.label(text='You are about to delete all stored custom bone naming schemes.', icon='ERROR')
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.scale_y = 0.5
|
||||
row.label(text='Continue?', icon='BLANK1')
|
||||
|
||||
layout.separator()
|
||||
|
||||
def invoke(self, context, event):
|
||||
return context.window_manager.invoke_props_dialog(self, width=400)
|
||||
|
||||
def execute(self, context):
|
||||
custom_schemes_manager.delete_custom_bone_list()
|
||||
|
||||
self.report({'INFO'}, 'Cleared all custom bone naming schemes!')
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class ClearCustomShapes(bpy.types.Operator):
|
||||
bl_idname = "rsl.clear_custom_shapes"
|
||||
bl_label = "Clear Custom Shapekeys"
|
||||
bl_description = "Clear all custom shape naming schemes"
|
||||
bl_options = {'INTERNAL'}
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
|
||||
layout.separator()
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.scale_y = 0.5
|
||||
row.label(text='You are about to delete all stored custom shape naming schemes.', icon='ERROR')
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.scale_y = 0.5
|
||||
row.label(text='Continue?', icon='BLANK1')
|
||||
|
||||
layout.separator()
|
||||
|
||||
def invoke(self, context, event):
|
||||
return context.window_manager.invoke_props_dialog(self, width=400)
|
||||
|
||||
def execute(self, context):
|
||||
custom_schemes_manager.delete_custom_shape_list()
|
||||
|
||||
self.report({'INFO'}, 'Cleared all custom shape naming schemes!')
|
||||
return {'FINISHED'}
|
||||
@@ -0,0 +1,63 @@
|
||||
import bpy
|
||||
import webbrowser
|
||||
|
||||
from ..core import login_manager as lm
|
||||
|
||||
|
||||
class LicenseButton(bpy.types.Operator):
|
||||
bl_idname = 'rsl.info_license'
|
||||
bl_label = 'License'
|
||||
bl_description = 'Opens the license in the browser'
|
||||
bl_options = {'INTERNAL'}
|
||||
|
||||
def execute(self, context):
|
||||
webbrowser.open('https://github.com/RokokoElectronics/rokoko-studio-live-blender/blob/master/LICENSE.md')
|
||||
self.report({'INFO'}, 'Opened license.')
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class RokokoButton(bpy.types.Operator):
|
||||
bl_idname = 'rsl.info_rokoko'
|
||||
bl_label = 'Website'
|
||||
bl_description = 'Opens the Rokoko website in the browser'
|
||||
bl_options = {'INTERNAL'}
|
||||
|
||||
def execute(self, context):
|
||||
webbrowser.open('https://www.rokoko.com/en')
|
||||
self.report({'INFO'}, 'Opened Rokoko website.')
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class DocumentationButton(bpy.types.Operator):
|
||||
bl_idname = 'rsl.info_documentation'
|
||||
bl_label = 'Documentation'
|
||||
bl_description = 'Opens the documentation in the browser'
|
||||
bl_options = {'INTERNAL'}
|
||||
|
||||
def execute(self, context):
|
||||
webbrowser.open('https://github.com/Rokoko/rokoko-studio-live-blender#readme')
|
||||
self.report({'INFO'}, 'Opened documentation.')
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class ForumButton(bpy.types.Operator):
|
||||
bl_idname = 'rsl.info_forum'
|
||||
bl_label = 'Join our Forums'
|
||||
bl_description = 'Opens the Rokoko Blender forum in the browser'
|
||||
bl_options = {'INTERNAL'}
|
||||
|
||||
def execute(self, context):
|
||||
webbrowser.open('https://rokoko.freshdesk.com/support/discussions/forums/47000399880')
|
||||
self.report({'INFO'}, 'Opened forums.')
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class ToggleRokokoIDButton(bpy.types.Operator):
|
||||
bl_idname = 'rsl.toggle_rokoko_id'
|
||||
bl_label = 'Toggle Rokoko ID'
|
||||
bl_description = 'Toggles the visibility of your Rokoko ID'
|
||||
bl_options = {'INTERNAL'}
|
||||
|
||||
def execute(self, context):
|
||||
lm.user.display_email = not lm.user.display_email
|
||||
return {'FINISHED'}
|
||||
@@ -0,0 +1,87 @@
|
||||
import traceback
|
||||
|
||||
import bpy
|
||||
import importlib
|
||||
from ..core import login_manager
|
||||
from ..core import library_manager
|
||||
from ..core import live_data_manager
|
||||
|
||||
|
||||
class LoginButton(bpy.types.Operator):
|
||||
bl_idname = "rsl.login_login"
|
||||
bl_label = "Sign in"
|
||||
bl_description = "Sign into your Rokoko account with your browser"
|
||||
bl_options = {'INTERNAL'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return not login_manager.user.logging_in
|
||||
|
||||
def execute(self, context):
|
||||
login = login_manager.Login()
|
||||
login.start()
|
||||
self.report({'INFO'}, 'Opened Rokoko ID website in your browser.')
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class LogoutButton(bpy.types.Operator):
|
||||
bl_idname = "rsl.login_logout"
|
||||
bl_label = "Sign out"
|
||||
bl_description = "Sign out of your Rokoko account"
|
||||
bl_options = {'INTERNAL'}
|
||||
|
||||
def execute(self, context):
|
||||
login_manager.user.logout()
|
||||
self.report({'INFO'}, 'Logout successful.')
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class InstallLibsButton(bpy.types.Operator):
|
||||
bl_idname = 'rsl.login_install_libs'
|
||||
bl_label = 'Install Required Libraries'
|
||||
bl_description = 'Installs the required libraries for this plugin to work'
|
||||
bl_options = {'INTERNAL'}
|
||||
|
||||
def execute(self, context):
|
||||
# Install the libraries
|
||||
try:
|
||||
self.install_libs()
|
||||
except ImportError as e:
|
||||
self.report({'ERROR'}, str(e))
|
||||
return {'CANCELLED'}
|
||||
except Exception as e:
|
||||
trace = traceback.format_exc()
|
||||
error_str = f"Unable to install the libraries!" \
|
||||
f"\nTry running Blender as an admin and install the libraries again." \
|
||||
f"\n\nFull Error: \n\n{trace}"
|
||||
self.report({'ERROR'}, error_str)
|
||||
return {'CANCELLED'}
|
||||
|
||||
# Save login manager data
|
||||
classes_logged_in = login_manager.user.classes_logged_in
|
||||
classes_logged_out = login_manager.user.classes_logged_out
|
||||
version_str = login_manager.user.version_str
|
||||
|
||||
# Reload files to load the libraries
|
||||
importlib.reload(login_manager)
|
||||
importlib.reload(live_data_manager)
|
||||
|
||||
# Load the login manager data
|
||||
login_manager.user.classes_logged_in = classes_logged_in
|
||||
login_manager.user.classes_logged_out = classes_logged_out
|
||||
login_manager.user.version_str = version_str
|
||||
|
||||
# Attempt to auto login the user
|
||||
login_manager.user.auto_login()
|
||||
|
||||
self.report({'INFO'}, 'Installed libraries successfully!')
|
||||
return {'FINISHED'}
|
||||
|
||||
def install_libs(self):
|
||||
missing = library_manager.lib_manager.install_libraries(["websockets", "gql", "cryptography", "boto3"])
|
||||
if missing:
|
||||
raise ImportError("The following libraries could not be installed: "
|
||||
"\n- " + " \n- ".join(missing) +
|
||||
" \n\nTry running Blender as an admin and install the libraries again."
|
||||
" \nSee console for more information.")
|
||||
library_manager.lib_manager.install_libraries(["lz4"])
|
||||
@@ -0,0 +1,100 @@
|
||||
import bpy
|
||||
import time
|
||||
from threading import Thread
|
||||
|
||||
from ..core import state_manager
|
||||
from ..core.receiver import Receiver
|
||||
from ..core.utils import ui_refresh_all
|
||||
from ..core.animations import clear_animations
|
||||
|
||||
timer = None
|
||||
receiver: Receiver = Receiver()
|
||||
receiver_enabled = False
|
||||
|
||||
|
||||
class ReceiverStart(bpy.types.Operator):
|
||||
bl_idname = "rsl.receiver_start"
|
||||
bl_label = "Start Receiver"
|
||||
bl_description = "Start receiving data from Rokoko Studio"
|
||||
bl_options = {'INTERNAL'}
|
||||
|
||||
def modal(self, context, event):
|
||||
# If ECS or F8 is pressed, cancel
|
||||
if event.type == 'ESC' or event.type == 'F8' or not receiver_enabled:
|
||||
return self.cancel(context)
|
||||
|
||||
# This gets run every frame
|
||||
if event.type == 'TIMER':
|
||||
if bpy.context.screen.is_animation_playing:
|
||||
return self.cancel(context)
|
||||
|
||||
receiver.run()
|
||||
|
||||
return {'PASS_THROUGH'}
|
||||
|
||||
def execute(self, context):
|
||||
global receiver_enabled, receiver, timer
|
||||
|
||||
# Start the receiver
|
||||
try:
|
||||
receiver.start(context.scene.rsl_receiver_port)
|
||||
except OSError as e:
|
||||
print('Socket error:', e.strerror)
|
||||
self.report({'ERROR'}, 'This port is already in use!')
|
||||
return {'CANCELLED'}
|
||||
|
||||
receiver_enabled = True
|
||||
|
||||
# If animation is currently playing, stop it
|
||||
if context.screen.is_animation_playing:
|
||||
bpy.ops.screen.animation_play()
|
||||
|
||||
# Clear current live data
|
||||
clear_animations()
|
||||
|
||||
# Save the scene
|
||||
state_manager.save_scene()
|
||||
|
||||
# Register this classes modal operator in Blenders event handling system and execute it at the specified fps
|
||||
context.window_manager.modal_handler_add(self)
|
||||
timer = context.window_manager.event_timer_add(1 / context.scene.rsl_receiver_fps, window=bpy.context.window)
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
def cancel(self, context):
|
||||
ReceiverStart.force_disable()
|
||||
ui_refresh_all()
|
||||
return {'CANCELLED'}
|
||||
|
||||
@classmethod
|
||||
def force_disable(cls):
|
||||
global receiver_enabled, receiver, timer
|
||||
|
||||
receiver_enabled = False
|
||||
receiver.stop()
|
||||
|
||||
bpy.context.window_manager.event_timer_remove(timer)
|
||||
|
||||
# If the recording is still running, let it load the scene afterwards with a delay
|
||||
if bpy.context.scene.rsl_recording:
|
||||
bpy.context.scene.rsl_recording = False
|
||||
thread = Thread(target=load_scene_later, args=[])
|
||||
thread.start()
|
||||
else:
|
||||
state_manager.load_scene()
|
||||
|
||||
|
||||
def load_scene_later():
|
||||
time.sleep(0.04)
|
||||
state_manager.load_scene()
|
||||
|
||||
|
||||
class ReceiverStop(bpy.types.Operator):
|
||||
bl_idname = "rsl.receiver_stop"
|
||||
bl_label = "Stop Receiver"
|
||||
bl_description = "Stop receiving data from Rokoko Studio"
|
||||
bl_options = {'INTERNAL'}
|
||||
|
||||
def execute(self, context):
|
||||
global receiver_enabled
|
||||
receiver_enabled = False
|
||||
return {'FINISHED'}
|
||||
@@ -0,0 +1,32 @@
|
||||
import bpy
|
||||
|
||||
|
||||
class RecorderStart(bpy.types.Operator):
|
||||
bl_idname = "rsl.recorder_start"
|
||||
bl_label = "Start Recording"
|
||||
bl_description = "Start recording data from Rokoko Studio"
|
||||
bl_options = {'INTERNAL'}
|
||||
|
||||
def execute(self, context):
|
||||
if context.scene.rsl_recording:
|
||||
self.report({'ERROR'}, 'Already recording')
|
||||
return {'CANCELLED'}
|
||||
|
||||
context.scene.rsl_recording = True
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class RecorderStop(bpy.types.Operator):
|
||||
bl_idname = "rsl.recorder_stop"
|
||||
bl_label = "Stop Recording"
|
||||
bl_description = "Stop recording data from Rokoko Studio" \
|
||||
"\nThe processing of the recording can take a couple minutes, depending on the length of the recording"
|
||||
bl_options = {'INTERNAL'}
|
||||
|
||||
def execute(self, context):
|
||||
if not context.scene.rsl_recording:
|
||||
self.report({'ERROR'}, 'Not recording')
|
||||
return {'CANCELLED'}
|
||||
|
||||
context.scene.rsl_recording = False
|
||||
return {'FINISHED'}
|
||||
@@ -0,0 +1,536 @@
|
||||
import bpy
|
||||
import copy
|
||||
|
||||
from . import detector
|
||||
from ..core import utils
|
||||
from ..core.retargeting import get_source_armature, get_target_armature
|
||||
from ..core import detection_manager as detector
|
||||
from ..core import custom_schemes_manager
|
||||
from ..panels.retargeting import BoneListItem
|
||||
|
||||
RETARGET_ID = '_RSL_RETARGET'
|
||||
|
||||
|
||||
class BuildBoneList(bpy.types.Operator):
|
||||
bl_idname = "rsl.build_bone_list"
|
||||
bl_label = "Build Bone List"
|
||||
bl_description = "Builds the bone list from the animation and tries to automatically detect and match bones"
|
||||
bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}
|
||||
|
||||
def execute(self, context):
|
||||
armature_source = get_source_armature()
|
||||
armature_target = get_target_armature()
|
||||
|
||||
if not armature_source.animation_data or not armature_source.animation_data.action:
|
||||
self.report({'ERROR'}, 'No animation on the source armature found!'
|
||||
'\nSelect an armature with an animation as source.')
|
||||
return {'CANCELLED'}
|
||||
|
||||
if armature_source.name == armature_target.name:
|
||||
self.report({'ERROR'}, 'Source and target armature are the same!'
|
||||
'\nPlease select different armatures.')
|
||||
return {'CANCELLED'}
|
||||
|
||||
retargeting_dict = detector.detect_retarget_bones()
|
||||
|
||||
# Clear the bone retargeting list
|
||||
context.scene.rsl_retargeting_bone_list.clear()
|
||||
|
||||
for bone_source, bone_values in retargeting_dict.items():
|
||||
bone_target, bone_key = bone_values
|
||||
|
||||
bone_item = context.scene.rsl_retargeting_bone_list.add()
|
||||
bone_item.bone_name_key = bone_key
|
||||
bone_item.bone_name_source = bone_source
|
||||
bone_item.bone_name_target = bone_target
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class AddBoneListItem(bpy.types.Operator):
|
||||
bl_idname = "rsl.add_bone_list_item"
|
||||
bl_label = "Add Bone List Item"
|
||||
bl_description = "Adds a customizable bone list item"
|
||||
bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}
|
||||
|
||||
def execute(self, context):
|
||||
bone_item = context.scene.rsl_retargeting_bone_list.add()
|
||||
bone_item.is_custom = True
|
||||
|
||||
context.scene.rsl_retargeting_bone_list_index = len(context.scene.rsl_retargeting_bone_list) - 1
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class ClearBoneList(bpy.types.Operator):
|
||||
bl_idname = "rsl.clear_bone_list"
|
||||
bl_label = "Clear Bone List"
|
||||
bl_description = "Clears the bone list so that you can manually fill in all bones"
|
||||
bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}
|
||||
|
||||
def execute(self, context):
|
||||
for bone_item in context.scene.rsl_retargeting_bone_list:
|
||||
bone_item.bone_name_target = ''
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class RetargetAnimation(bpy.types.Operator):
|
||||
bl_idname = "rsl.retarget_animation"
|
||||
bl_label = "Retarget Animation"
|
||||
bl_description = "Retargets the animation from the source armature to the target armature"
|
||||
bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}
|
||||
|
||||
retarget_bone_list: [BoneListItem] = []
|
||||
|
||||
def execute(self, context):
|
||||
armature_source = get_source_armature()
|
||||
armature_target = get_target_armature()
|
||||
|
||||
if not armature_source.animation_data or not armature_source.animation_data.action:
|
||||
self.report({'ERROR'}, 'No animation on the source armature found!'
|
||||
'\nSelect an armature with an animation as source.')
|
||||
return {'CANCELLED'}
|
||||
|
||||
if armature_source.name == armature_target.name:
|
||||
self.report({'ERROR'}, 'Source and target armature are the same!'
|
||||
'\nPlease select different armatures.')
|
||||
return {'CANCELLED'}
|
||||
|
||||
# Build retargeting bone list
|
||||
self.retarget_bone_list.clear()
|
||||
for item in context.scene.rsl_retargeting_bone_list:
|
||||
if not item.bone_name_source or not item.bone_name_target \
|
||||
or not armature_source.pose.bones.get(item.bone_name_source) \
|
||||
or not armature_target.pose.bones.get(item.bone_name_target):
|
||||
continue
|
||||
self.retarget_bone_list.append(item)
|
||||
|
||||
# Find the root bones and cancel if none are found
|
||||
root_bones = self.find_root_bones(context, armature_source, armature_target)
|
||||
if not root_bones:
|
||||
self.report({'ERROR'}, 'No root bone found!'
|
||||
'\nCheck if the bones are mapped correctly or try rebuilding the bone list.')
|
||||
return {'CANCELLED'}
|
||||
|
||||
# Check for duplicate target bone entries
|
||||
seen = {}
|
||||
for item in self.retarget_bone_list:
|
||||
count = seen.get(item.bone_name_target)
|
||||
if not count:
|
||||
count = 0
|
||||
seen[item.bone_name_target] = count + 1
|
||||
duplicates = [key for key, value in seen.items() if value > 1]
|
||||
if duplicates:
|
||||
self.report({'ERROR'}, 'Duplicate target bone entries found! Please use each target bone only once:'
|
||||
f'\n{", ".join(duplicates)}')
|
||||
return {'CANCELLED'}
|
||||
|
||||
# Save the bone list if the user changed anything
|
||||
custom_schemes_manager.save_retargeting_to_list()
|
||||
|
||||
# Prepare armatures
|
||||
utils.set_active(armature_target)
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
utils.set_active(armature_source)
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
|
||||
# Set armatures into pose mode
|
||||
armature_source.data.pose_position = 'POSE'
|
||||
armature_target.data.pose_position = 'POSE'
|
||||
|
||||
# Save and reset the current pose position of both armatures if rest position should be used
|
||||
pose_source, pose_target = {}, {}
|
||||
if bpy.context.scene.rsl_retargeting_use_pose == 'REST':
|
||||
pose_source = self.get_and_reset_pose_rotations(armature_source)
|
||||
pose_target = self.get_and_reset_pose_rotations(armature_target)
|
||||
|
||||
# Auto scaling
|
||||
source_scale = None
|
||||
if context.scene.rsl_retargeting_auto_scaling:
|
||||
# Clean source animation
|
||||
# TODO: This causes issues when all Hip bone data is on the armature itself
|
||||
self.clean_animation(armature_source)
|
||||
|
||||
# Scale the source armature to fit the target armature
|
||||
source_scale = copy.deepcopy(armature_source.scale)
|
||||
self.scale_armature(context, armature_source, armature_target, root_bones)
|
||||
|
||||
# Duplicate source armature to apply transforms to the animation
|
||||
armature_source_original = armature_source
|
||||
armature_source = self.copy_rest_pose(context, armature_source)
|
||||
|
||||
# Save transforms of target armature
|
||||
rotation_mode = armature_target.rotation_mode
|
||||
armature_target.rotation_mode = 'QUATERNION'
|
||||
rotation = copy.deepcopy(armature_target.rotation_quaternion)
|
||||
location = copy.deepcopy(armature_target.location)
|
||||
|
||||
# Apply transforms of the target armature
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
utils.set_active(armature_target)
|
||||
bpy.ops.object.transform_apply(location=True, rotation=True, scale=True)
|
||||
|
||||
bpy.ops.object.mode_set(mode='EDIT')
|
||||
|
||||
# Create a transformation dict of all bones of the target armature and unselect all bones
|
||||
bone_transforms = {}
|
||||
for bone in context.object.data.edit_bones:
|
||||
bone.select = False
|
||||
bone_transforms[bone.name] = armature_source.matrix_world.inverted() @ bone.head.copy(), \
|
||||
armature_source.matrix_world.inverted() @ bone.tail.copy(), \
|
||||
utils.mat3_to_vec_roll(armature_source.matrix_world.inverted().to_3x3() @ bone.matrix.to_3x3()) # Head loc, tail loc, bone roll
|
||||
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
utils.set_active(armature_source)
|
||||
bpy.ops.object.mode_set(mode='EDIT')
|
||||
|
||||
# Recreate bones from target armature in source armature
|
||||
for item in self.retarget_bone_list:
|
||||
bone_source = armature_source.data.edit_bones.get(item.bone_name_source)
|
||||
|
||||
# Recreate target bone
|
||||
bone_new = armature_source.data.edit_bones.new(item.bone_name_target + RETARGET_ID)
|
||||
bone_new.head, bone_new.tail, bone_new.roll = bone_transforms[item.bone_name_target]
|
||||
bone_new.parent = bone_source
|
||||
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
|
||||
# Add constraints to target armature and select the bones for animation
|
||||
for item in self.retarget_bone_list:
|
||||
bone_target = armature_target.pose.bones.get(item.bone_name_target)
|
||||
|
||||
# Add constraints
|
||||
constraint = bone_target.constraints.new('COPY_ROTATION')
|
||||
constraint.name += RETARGET_ID
|
||||
constraint.target = armature_source
|
||||
constraint.subtarget = item.bone_name_target + RETARGET_ID
|
||||
|
||||
if bone_target.name in root_bones:
|
||||
constraint = bone_target.constraints.new('COPY_LOCATION')
|
||||
constraint.name += RETARGET_ID
|
||||
constraint.target = armature_source
|
||||
constraint.subtarget = item.bone_name_source
|
||||
|
||||
# Select the bone for animation
|
||||
armature_target.data.bones.get(item.bone_name_target).select = True
|
||||
|
||||
# Bake the animation to the target armature
|
||||
self.bake_animation(armature_source, armature_target, root_bones)
|
||||
|
||||
# Delete the duplicate helper armature
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
utils.set_active(armature_source)
|
||||
bpy.data.actions.remove(armature_source.animation_data.action)
|
||||
bpy.ops.object.delete()
|
||||
|
||||
# Change armature source back to original
|
||||
armature_source = armature_source_original
|
||||
|
||||
# Change action name
|
||||
armature_target.animation_data.action.name = armature_source.animation_data.action.name + ' Retarget'
|
||||
|
||||
# Remove constraints from target armature
|
||||
for bone in armature_target.pose.bones:
|
||||
for constraint in bone.constraints:
|
||||
if RETARGET_ID in constraint.name:
|
||||
bone.constraints.remove(constraint)
|
||||
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
utils.set_active(armature_target)
|
||||
|
||||
# Reset target armature transforms to old state
|
||||
armature_target.rotation_quaternion = rotation
|
||||
armature_target.location = location
|
||||
|
||||
armature_target.rotation_quaternion.w = -armature_target.rotation_quaternion.w
|
||||
bpy.ops.object.transform_apply(location=False, rotation=True, scale=False)
|
||||
armature_target.rotation_quaternion = rotation
|
||||
armature_target.rotation_mode = rotation_mode
|
||||
|
||||
# Reset source armature scale
|
||||
if source_scale:
|
||||
armature_source.scale = source_scale
|
||||
|
||||
# Reset pose positions to old state
|
||||
# self.load_pose_rotations(armature_source, pose_source)
|
||||
# self.load_pose_rotations(armature_target, pose_target)
|
||||
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
|
||||
self.report({'INFO'}, 'Retargeted animation.')
|
||||
return {'FINISHED'}
|
||||
|
||||
def find_root_bones(self, context, armature_source, armature_target):
|
||||
# Find all root bones
|
||||
root_bones = []
|
||||
for bone in armature_target.pose.bones:
|
||||
if not bone.parent:
|
||||
root_bones.append(bone)
|
||||
|
||||
# Find animated root bones
|
||||
root_bones_animated = []
|
||||
target_bones = [item.bone_name_target for item in self.retarget_bone_list]
|
||||
while root_bones:
|
||||
for bone in copy.copy(root_bones):
|
||||
root_bones.remove(bone)
|
||||
if bone.name in target_bones:
|
||||
root_bones_animated.append(bone.name)
|
||||
else:
|
||||
for bone_child in bone.children:
|
||||
root_bones.append(bone_child)
|
||||
return root_bones_animated
|
||||
|
||||
def clean_animation(self, armature_source):
|
||||
deletable_fcurves = ['location', 'rotation_euler', 'rotation_quaternion', 'scale']
|
||||
for fcurve in armature_source.animation_data.action.fcurves:
|
||||
if fcurve.data_path in deletable_fcurves:
|
||||
armature_source.animation_data.action.fcurves.remove(fcurve)
|
||||
|
||||
def get_and_reset_pose_rotations(self, armature):
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
utils.set_active(armature)
|
||||
bpy.ops.object.mode_set(mode='POSE')
|
||||
|
||||
# Save rotations
|
||||
pose_rotations = {}
|
||||
for bone in armature.pose.bones:
|
||||
if bone.rotation_mode == 'QUATERNION':
|
||||
pose_rotations[bone.name] = copy.deepcopy(bone.rotation_quaternion)
|
||||
bone.rotation_quaternion = (1, 0, 0, 0)
|
||||
else:
|
||||
pose_rotations[bone.name] = copy.deepcopy(bone.rotation_euler)
|
||||
bone.rotation_euler = (0, 0, 0)
|
||||
|
||||
# Reset rotations
|
||||
# bpy.ops.pose.rot_clear()
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
|
||||
return pose_rotations
|
||||
|
||||
def load_pose_rotations(self, armature, pose_rotations):
|
||||
if not pose_rotations:
|
||||
return
|
||||
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
utils.set_active(armature)
|
||||
bpy.ops.object.mode_set(mode='POSE')
|
||||
|
||||
# Load rotations
|
||||
for bone in armature.pose.bones:
|
||||
rot = pose_rotations.get(bone.name)
|
||||
if rot:
|
||||
if bone.rotation_mode == 'QUATERNION':
|
||||
bone.rotation_quaternion = rot
|
||||
else:
|
||||
bone.rotation_euler = rot
|
||||
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
|
||||
def scale_armature(self, context, armature_source, armature_target, root_bones):
|
||||
source_min = None
|
||||
source_min_root = None
|
||||
target_min = None
|
||||
target_min_root = None
|
||||
|
||||
for item in self.retarget_bone_list:
|
||||
bone_source = armature_source.pose.bones.get(item.bone_name_source)
|
||||
bone_target = armature_target.pose.bones.get(item.bone_name_target)
|
||||
|
||||
bone_source_z = (armature_source.matrix_world @ bone_source.head)[2]
|
||||
bone_target_z = (armature_target.matrix_world @ bone_target.head)[2]
|
||||
|
||||
if item.bone_name_target in root_bones:
|
||||
if source_min_root is None or source_min_root > bone_source_z:
|
||||
source_min_root = bone_source_z
|
||||
if target_min_root is None or target_min_root > bone_target_z:
|
||||
target_min_root = bone_target_z
|
||||
|
||||
if source_min is None or source_min > bone_source_z:
|
||||
source_min = bone_source_z
|
||||
if target_min is None or target_min > bone_target_z:
|
||||
target_min = bone_target_z
|
||||
|
||||
source_height = source_min_root - source_min
|
||||
target_height = target_min_root - target_min
|
||||
|
||||
if not source_height or not target_height:
|
||||
print('No scaling needed')
|
||||
return
|
||||
|
||||
scale_factor = target_height / source_height
|
||||
armature_source.scale *= scale_factor
|
||||
|
||||
def read_anim_start_end(self, armature):
|
||||
frame_start = None
|
||||
frame_end = None
|
||||
for fcurve in armature.animation_data.action.fcurves:
|
||||
for key in fcurve.keyframe_points:
|
||||
keyframe = key.co.x
|
||||
if frame_start is None:
|
||||
frame_start = keyframe
|
||||
if frame_end is None:
|
||||
frame_end = keyframe
|
||||
|
||||
if keyframe < frame_start:
|
||||
frame_start = keyframe
|
||||
if keyframe > frame_end:
|
||||
frame_end = keyframe
|
||||
|
||||
return frame_start, frame_end
|
||||
|
||||
def copy_rest_pose(self, context, armature_source):
|
||||
# make sure auto keyframe is disabled, leads to issues
|
||||
context.scene.tool_settings.use_keyframe_insert_auto = False
|
||||
|
||||
# ensure the source armature selection
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
utils.set_active(armature_source)
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
|
||||
# Duplicate the source armature
|
||||
bpy.ops.object.duplicate_move(OBJECT_OT_duplicate={"linked": False, "mode": 'TRANSLATION'},
|
||||
TRANSFORM_OT_translate={"value": (0, 0, 0), "constraint_axis": (False, True, False), "mirror": False, "snap": False, "remove_on_cancel": False,
|
||||
"release_confirm": False})
|
||||
|
||||
# Set name of the copied source armature
|
||||
source_armature_copy = context.object
|
||||
source_armature_copy.name = armature_source.name + "_copy"
|
||||
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
utils.set_active(source_armature_copy)
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
bpy.ops.object.mode_set(mode='POSE')
|
||||
|
||||
# Apply transforms of the new source armature. Unlink action temporarily to prevent warning in console
|
||||
action_tmp = source_armature_copy.animation_data.action
|
||||
source_armature_copy.animation_data.action = None
|
||||
bpy.ops.pose.armature_apply()
|
||||
source_armature_copy.animation_data.action = action_tmp
|
||||
|
||||
# Mimic the animation of the original source armature by adding constraints to the bones.
|
||||
# -> the new armature has the exact same animation but with applied transforms
|
||||
for bone in source_armature_copy.pose.bones:
|
||||
constraint = bone.constraints.new('COPY_TRANSFORMS')
|
||||
constraint.name = bone.name
|
||||
constraint.target = armature_source
|
||||
constraint.subtarget = bone.name
|
||||
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
|
||||
return source_armature_copy
|
||||
|
||||
def bake_animation(self, armature_source, armature_target, root_bones):
|
||||
frame_split = 25
|
||||
frame_start, frame_end = self.read_anim_start_end(armature_source)
|
||||
frame_start, frame_end = int(frame_start), int(frame_end)
|
||||
utils.set_active(armature_target)
|
||||
|
||||
actions_all = []
|
||||
|
||||
# Setup loading bar
|
||||
current_step = 0
|
||||
steps = int((frame_end - frame_start) / frame_split) + 1
|
||||
wm = bpy.context.window_manager
|
||||
wm.progress_begin(current_step, steps)
|
||||
|
||||
import time
|
||||
start_time = time.time()
|
||||
|
||||
# Bake the animation in parts because multiple short parts are processed much faster than one long animation
|
||||
bpy.ops.object.mode_set(mode='POSE')
|
||||
for frame in range(frame_start, frame_end + 2, frame_split):
|
||||
start = frame
|
||||
end = frame + frame_split - 1
|
||||
if end > frame_end:
|
||||
end = frame_end
|
||||
if start > end:
|
||||
continue
|
||||
|
||||
# Bake animation part
|
||||
bpy.ops.nla.bake(frame_start=start, frame_end=end, visual_keying=True, only_selected=True, use_current_action=False, bake_types={'POSE'})
|
||||
|
||||
# Rename animation part
|
||||
armature_target.animation_data.action.name = 'RSL_RETARGETING_' + str(frame)
|
||||
|
||||
actions_all.append(armature_target.animation_data.action)
|
||||
|
||||
current_step += 1
|
||||
if steps != current_step:
|
||||
wm.progress_update(current_step)
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
|
||||
if not actions_all:
|
||||
return
|
||||
|
||||
# Count all keys for all data_paths
|
||||
key_counts = {}
|
||||
for action in actions_all:
|
||||
for fcurve in action.fcurves:
|
||||
key = fcurve.data_path + str(fcurve.array_index)
|
||||
if not key_counts.get(key):
|
||||
key_counts[key] = 0
|
||||
key_counts[key] += len(fcurve.keyframe_points)
|
||||
|
||||
# Create new action
|
||||
action_final = bpy.data.actions.new(name='RSL_RETARGETING_FINAL')
|
||||
action_final.use_fake_user = True
|
||||
armature_target.animation_data_create().action = action_final
|
||||
|
||||
# Put all baked animations parts back together into one
|
||||
print_i = 0
|
||||
for fcurve in actions_all[0].fcurves:
|
||||
if fcurve.data_path.endswith('scale'):
|
||||
continue
|
||||
if fcurve.data_path.endswith('location'):
|
||||
bone_name = fcurve.data_path.split('"')
|
||||
if len(bone_name) != 3:
|
||||
continue
|
||||
if bone_name[1] not in root_bones:
|
||||
continue
|
||||
|
||||
curve_final = action_final.fcurves.new(data_path=fcurve.data_path, index=fcurve.array_index, action_group=fcurve.group.name)
|
||||
keyframe_points = curve_final.keyframe_points
|
||||
keyframe_points.add(key_counts[fcurve.data_path + str(fcurve.array_index)])
|
||||
|
||||
index = 0
|
||||
for action in actions_all:
|
||||
fcruve_to_add = action.fcurves.find(data_path=fcurve.data_path, index=fcurve.array_index)
|
||||
|
||||
for kp in fcruve_to_add.keyframe_points:
|
||||
keyframe_points[index].co.x = kp.co.x
|
||||
keyframe_points[index].co.y = kp.co.y
|
||||
keyframe_points[index].interpolation = 'LINEAR'
|
||||
index += 1
|
||||
|
||||
print_i += 1
|
||||
|
||||
# Clean up animation. Delete all keyframes the use the same value as the previous and next one
|
||||
for fcurve in action_final.fcurves:
|
||||
if len(fcurve.keyframe_points) <= 2:
|
||||
continue
|
||||
|
||||
kp_pre_pre = fcurve.keyframe_points[0]
|
||||
kp_pre = fcurve.keyframe_points[1]
|
||||
|
||||
kp_to_delete = []
|
||||
for kp in fcurve.keyframe_points[2:]:
|
||||
if round(kp_pre_pre.co.y, 5) == round(kp_pre.co.y, 5) == round(kp.co.y, 5):
|
||||
kp_to_delete.append(kp_pre)
|
||||
kp_pre_pre = kp_pre
|
||||
kp_pre = kp
|
||||
|
||||
for kp in reversed(kp_to_delete):
|
||||
fcurve.keyframe_points.remove(kp)
|
||||
|
||||
# Delete all baked animation parts, only the combined one is needed
|
||||
for action in actions_all:
|
||||
bpy.data.actions.remove(action)
|
||||
|
||||
print('Retargeting Time:', round(time.time() - start_time, 2), 'seconds')
|
||||
wm.progress_end()
|
||||
|
||||
# Set the action slot sub action
|
||||
if hasattr(armature_target.animation_data, "action_slot"):
|
||||
armature_target.animation_data.action_slot = armature_target.animation_data.action_suitable_slots[0]
|
||||
@@ -0,0 +1,19 @@
|
||||
if "bpy" not in locals():
|
||||
import bpy
|
||||
from . import main
|
||||
from . import objects
|
||||
from . import command_api
|
||||
from . import retargeting
|
||||
from . import updater
|
||||
from . import info
|
||||
from . import login
|
||||
else:
|
||||
import importlib
|
||||
|
||||
importlib.reload(main)
|
||||
importlib.reload(objects)
|
||||
importlib.reload(command_api)
|
||||
importlib.reload(retargeting)
|
||||
importlib.reload(updater)
|
||||
importlib.reload(info)
|
||||
importlib.reload(login)
|
||||
@@ -0,0 +1,37 @@
|
||||
import bpy
|
||||
|
||||
from .main import ToolPanel
|
||||
from ..operators import command_api
|
||||
from ..core.icon_manager import Icons
|
||||
|
||||
|
||||
# Main panel of the Rokoko panel
|
||||
class CommandPanel(ToolPanel, bpy.types.Panel):
|
||||
bl_idname = 'VIEW3D_PT_rsl_command_api_v2'
|
||||
bl_label = 'Studio Command API'
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.use_property_split = False
|
||||
|
||||
col = layout.column()
|
||||
|
||||
row = col.row(align=True)
|
||||
row.label(text='Address:')
|
||||
row.prop(context.scene, 'rsl_command_ip_address', text='')
|
||||
|
||||
row = col.row(align=True)
|
||||
row.label(text='Port:')
|
||||
row.prop(context.scene, 'rsl_command_ip_port', text='')
|
||||
|
||||
row = col.row(align=True)
|
||||
row.label(text='Key:')
|
||||
row.prop(context.scene, 'rsl_command_api_key', text='')
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.scale_y = 1.5
|
||||
row.scale_x = 3
|
||||
row.operator(command_api.StartCalibration.bl_idname, text='', icon_value=Icons.CALIBRATE.get_icon())
|
||||
row.operator(command_api.Restart.bl_idname, text='', icon_value=Icons.RESTART.get_icon())
|
||||
row.operator(command_api.StartRecording.bl_idname, text='', icon_value=Icons.START_RECORDING.get_icon())
|
||||
row.operator(command_api.StopRecording.bl_idname, text='', icon='SNAP_FACE')
|
||||
@@ -0,0 +1,64 @@
|
||||
import bpy
|
||||
|
||||
from .main import ToolPanel, separator
|
||||
from .. import updater
|
||||
from ..operators import info
|
||||
from ..core.icon_manager import Icons
|
||||
from ..operators.login import LogoutButton
|
||||
from ..core import login_manager as lm
|
||||
|
||||
|
||||
class InfoPanel(ToolPanel, bpy.types.Panel):
|
||||
bl_idname = 'VIEW3D_PT_rsl_info_v2'
|
||||
bl_label = 'Info'
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.label(text='Rokoko Studio Live', icon_value=Icons.STUDIO_LIVE_LOGO.get_icon())
|
||||
row = layout.row(align=True)
|
||||
row.scale_y = 0.1
|
||||
row.label(text='for Blender (v' + updater.current_version_str + ')', icon='BLANK1')
|
||||
|
||||
separator(layout, 0.01)
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.label(text='Developed by ', icon='BLANK1')
|
||||
row.scale_y = 0.6
|
||||
row = layout.row(align=True)
|
||||
row.scale_y = 0.3
|
||||
row.label(text='Rokoko Electronics ApS', icon='BLANK1')
|
||||
|
||||
separator(layout, 0.1)
|
||||
|
||||
col = layout.column(align=True)
|
||||
|
||||
row = col.row(align=True)
|
||||
row.operator(info.LicenseButton.bl_idname)
|
||||
row.operator(info.RokokoButton.bl_idname)
|
||||
row = col.row(align=True)
|
||||
row.operator(info.DocumentationButton.bl_idname)
|
||||
# row = col.row(align=True)
|
||||
# row.operator(info.ForumButton.bl_idname) # TODO: Add forums back with correct link
|
||||
|
||||
# If there is no email, the user is not logged in yet
|
||||
if not lm.user.email:
|
||||
return
|
||||
|
||||
separator(layout, 0.1)
|
||||
|
||||
subrow = layout.row(align=True)
|
||||
row = subrow.row(align=True)
|
||||
row.scale_y = 0.7
|
||||
row.label(text='Rokoko ID:')
|
||||
row = subrow.row(align=True)
|
||||
row.scale_y = 0.7
|
||||
row.alignment = 'RIGHT'
|
||||
row.operator(info.ToggleRokokoIDButton.bl_idname, text='', icon='HIDE_OFF' if lm.user.display_email else 'HIDE_ON')
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.scale_y = 0.3
|
||||
row.label(text=lm.user.email if lm.user.display_email else "***********")
|
||||
row = layout.row(align=True)
|
||||
row.operator(LogoutButton.bl_idname)
|
||||
@@ -0,0 +1,44 @@
|
||||
import bpy
|
||||
from .main import ToolPanel, separator
|
||||
from ..operators.login import LoginButton, InstallLibsButton
|
||||
from ..core.icon_manager import Icons
|
||||
from .. import updater, updater_ops
|
||||
from ..core import login_manager as lm
|
||||
|
||||
|
||||
class LoginPanel(ToolPanel, bpy.types.Panel):
|
||||
bl_idname = 'VIEW3D_PT_rsl_login_v2'
|
||||
bl_label = 'Rokoko ID'
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
|
||||
updater.check_for_update_background(check_on_startup=True)
|
||||
updater_ops.draw_update_notification_panel(layout)
|
||||
|
||||
if not lm.loaded_all_libs:
|
||||
row = layout.row(align=True)
|
||||
row.label(text="First time setup:", icon="INFO")
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.scale_y = 2
|
||||
row.operator(InstallLibsButton.bl_idname, icon="TRIA_DOWN_BAR")
|
||||
return
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.scale_y = 2
|
||||
row.operator(LoginButton.bl_idname, text="Sign in to Rokoko" if not lm.user.logging_in else "Waiting for sign in..", icon_value=Icons.STUDIO_LIVE_LOGO.get_icon())
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.scale_y = 0.5
|
||||
row.label(text='*Opens your browser')
|
||||
|
||||
errors = lm.user.display_error
|
||||
if not errors:
|
||||
return
|
||||
|
||||
separator(layout, scale=0.2)
|
||||
for i, error in enumerate(errors):
|
||||
row = layout.row(align=True)
|
||||
row.scale_y = 0.5
|
||||
row.label(text=error, icon="ERROR" if i == 0 else "BLANK1")
|
||||
@@ -0,0 +1,329 @@
|
||||
import bpy
|
||||
import datetime
|
||||
|
||||
from .. import updater, updater_ops
|
||||
from ..core import animations
|
||||
from ..core import recorder as recorder_manager
|
||||
from ..core import receiver as receiver_cls
|
||||
from ..core.icon_manager import Icons
|
||||
from ..operators import receiver, recorder
|
||||
|
||||
row_scale = 0.75
|
||||
paired_inputs = {}
|
||||
|
||||
|
||||
# Initializes the Rokoko panel in the toolbar
|
||||
class ToolPanel(object):
|
||||
bl_label = 'Rokoko'
|
||||
bl_idname = 'VIEW3D_TS_rokoko'
|
||||
bl_category = 'Rokoko'
|
||||
bl_space_type = 'VIEW_3D'
|
||||
bl_region_type = 'UI'
|
||||
|
||||
|
||||
def separator(layout, scale=1):
|
||||
# Add small separator
|
||||
row = layout.row(align=True)
|
||||
row.scale_y = scale
|
||||
row.label(text='')
|
||||
|
||||
|
||||
# Main panel of the Rokoko panel
|
||||
class ReceiverPanel(ToolPanel, bpy.types.Panel):
|
||||
bl_idname = 'VIEW3D_PT_rsl_receiver_v2'
|
||||
bl_label = 'Rokoko Studio Live'
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.use_property_split = False
|
||||
|
||||
# box = layout.box()
|
||||
updater.check_for_update_background(check_on_startup=True)
|
||||
updater_ops.draw_update_notification_panel(layout)
|
||||
|
||||
col = layout.column()
|
||||
|
||||
row = col.row(align=True)
|
||||
row.label(text='Port:')
|
||||
row.enabled = not receiver.receiver_enabled
|
||||
row.prop(context.scene, 'rsl_receiver_port', text='')
|
||||
|
||||
# row = col.row(align=True)
|
||||
# row.label(text='FPS:')
|
||||
# row.enabled = not receiver.receiver_enabled
|
||||
# row.prop(context.scene, 'rsl_receiver_fps', text='')
|
||||
|
||||
row = col.row(align=True)
|
||||
row.label(text='Scene Scale:')
|
||||
row.prop(context.scene, 'rsl_scene_scaling', text='')
|
||||
|
||||
layout.separator()
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.prop(context.scene, 'rsl_reset_scene_on_stop')
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.prop(context.scene, 'rsl_hide_mesh_during_play')
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.scale_y = 1.3
|
||||
if receiver.receiver_enabled:
|
||||
row.operator(receiver.ReceiverStop.bl_idname, icon='PAUSE', depress=True)
|
||||
else:
|
||||
row.operator(receiver.ReceiverStart.bl_idname, icon='PLAY')
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.scale_y = 1.3
|
||||
row.enabled = receiver.receiver_enabled
|
||||
if not context.scene.rsl_recording:
|
||||
row.operator(recorder.RecorderStart.bl_idname, icon_value=Icons.START_RECORDING.get_icon())
|
||||
else:
|
||||
row.operator(recorder.RecorderStop.bl_idname, icon='SNAP_FACE', depress=True)
|
||||
|
||||
# Calculate recording time
|
||||
timestamps = list(recorder_manager.recorded_timestamps.keys())
|
||||
if timestamps:
|
||||
time_recorded = int(timestamps[-1] - timestamps[0])
|
||||
row = layout.row(align=True)
|
||||
row.label(text='Recording time: ' + str(datetime.timedelta(seconds=time_recorded)))
|
||||
|
||||
if receiver.receiver_enabled and receiver_cls.show_error:
|
||||
for i, error in enumerate(receiver_cls.show_error):
|
||||
if i == 0:
|
||||
row = layout.row(align=True)
|
||||
row.label(text=error, icon='ERROR')
|
||||
else:
|
||||
row = layout.row(align=True)
|
||||
row.scale_y = 0.3
|
||||
row.label(text=error, icon='BLANK1')
|
||||
return
|
||||
|
||||
if animations.live_data.version <= 2:
|
||||
show_connetions_v2(layout)
|
||||
else:
|
||||
show_connetions_v3(layout)
|
||||
|
||||
|
||||
def show_connetions_v2(layout):
|
||||
# Show all inputs
|
||||
global paired_inputs
|
||||
paired_inputs = {}
|
||||
used_trackers = []
|
||||
used_faces = []
|
||||
|
||||
# Get all paired inputs. Paired inputs are paired to an object in the scene
|
||||
for obj in bpy.data.objects:
|
||||
# Get paired props and trackers
|
||||
if animations.live_data.props or animations.live_data.trackers:
|
||||
if obj.rsl_animations_props_trackers and obj.rsl_animations_props_trackers != 'None':
|
||||
paired = paired_inputs.get(obj.rsl_animations_props_trackers.split('|')[1])
|
||||
if not paired:
|
||||
paired_inputs[obj.rsl_animations_props_trackers.split('|')[1]] = [obj.name]
|
||||
else:
|
||||
paired.append(obj.name)
|
||||
|
||||
# Get paired faces
|
||||
if animations.live_data.faces and obj.rsl_animations_faces and obj.rsl_animations_faces != 'None':
|
||||
paired = paired_inputs.get(obj.rsl_animations_faces)
|
||||
if not paired:
|
||||
paired_inputs[obj.rsl_animations_faces] = [obj.name]
|
||||
else:
|
||||
paired.append(obj.name)
|
||||
|
||||
# Get paired actors
|
||||
if animations.live_data.actors and obj.rsl_animations_actors and obj.rsl_animations_actors != 'None':
|
||||
paired = paired_inputs.get(obj.rsl_animations_actors)
|
||||
if not paired:
|
||||
paired_inputs[obj.rsl_animations_actors] = [obj.name]
|
||||
else:
|
||||
paired.append(obj.name)
|
||||
|
||||
# This is used as a small spacer
|
||||
row = layout.row(align=True)
|
||||
row.scale_y = 0.01
|
||||
row.label(text=' ')
|
||||
|
||||
# Display all paired and unpaired inputs
|
||||
for actor in animations.live_data.actors:
|
||||
if actor['profileName']:
|
||||
row = layout.row(align=True)
|
||||
row.scale_y = row_scale
|
||||
row.label(text=actor['profileName'], icon='ANTIALIASED')
|
||||
|
||||
split = layout.row(align=True)
|
||||
split.scale_y = row_scale
|
||||
add_indent(split)
|
||||
show_actor(split, actor)
|
||||
|
||||
for tracker in animations.live_data.trackers:
|
||||
if tracker['connectionId'] == actor['name']:
|
||||
split = layout.row(align=True)
|
||||
split.scale_y = row_scale
|
||||
add_indent(split, empty=True)
|
||||
add_indent(split)
|
||||
show_tracker(split, tracker)
|
||||
used_trackers.append(tracker['name'])
|
||||
|
||||
for face in animations.live_data.faces:
|
||||
if face.get('profileName') and face.get('profileName') == actor['profileName']:
|
||||
split = layout.row(align=True)
|
||||
split.scale_y = row_scale
|
||||
add_indent(split)
|
||||
show_face(split, face)
|
||||
used_faces.append(face['faceId'])
|
||||
|
||||
# split = layout.row(align=True)
|
||||
# add_indent(split)
|
||||
# row = split.row(align=True)
|
||||
# row.label(text='faceId', icon_value=Icons.FACE.get_icon())
|
||||
|
||||
for prop in animations.live_data.props:
|
||||
show_prop(layout, prop, scale=True)
|
||||
|
||||
for tracker in animations.live_data.trackers:
|
||||
if tracker['connectionId'] == prop['id']:
|
||||
split = layout.row(align=True)
|
||||
split.scale_y = row_scale
|
||||
add_indent(split)
|
||||
show_tracker(split, tracker)
|
||||
used_trackers.append(tracker['name'])
|
||||
|
||||
for tracker in animations.live_data.trackers:
|
||||
if tracker['name'] not in used_trackers:
|
||||
show_tracker(layout, tracker, scale=True)
|
||||
|
||||
# row = layout.row(align=True)
|
||||
# row.label(text='5', icon_value=Icons.VP.get_icon())
|
||||
|
||||
for face in animations.live_data.faces:
|
||||
if face['faceId'] not in used_faces:
|
||||
show_face(layout, face, scale=True)
|
||||
|
||||
|
||||
def show_connetions_v3(layout):
|
||||
# Show all inputs
|
||||
global paired_inputs
|
||||
paired_inputs = {}
|
||||
|
||||
for obj in bpy.data.objects:
|
||||
# Get props
|
||||
if obj.rsl_animations_props_trackers and obj.rsl_animations_props_trackers != 'None':
|
||||
if animations.live_data.props:
|
||||
prop = animations.live_data.get_prop_by_obj(obj)
|
||||
if prop:
|
||||
prop_id = animations.live_data.get_prop_id(prop)
|
||||
if not paired_inputs.get(prop_id):
|
||||
paired_inputs[prop_id] = [obj.name]
|
||||
else:
|
||||
paired_inputs[prop_id].append(obj.name)
|
||||
|
||||
# Get faces
|
||||
if animations.live_data.faces and obj.rsl_animations_faces and obj.rsl_animations_faces != 'None':
|
||||
face = animations.live_data.get_face_by_obj(obj)
|
||||
if face:
|
||||
face_id = animations.live_data.get_face_id(face)
|
||||
if not paired_inputs.get(face_id):
|
||||
paired_inputs[face_id] = [obj.name]
|
||||
else:
|
||||
paired_inputs[face_id].append(obj.name)
|
||||
|
||||
# Get actors
|
||||
if animations.live_data.actors and obj.rsl_animations_actors and obj.rsl_animations_actors != 'None':
|
||||
actor = animations.live_data.get_actor_by_obj(obj)
|
||||
if actor:
|
||||
actor_id = animations.live_data.get_actor_id(actor)
|
||||
if not paired_inputs.get(actor_id):
|
||||
paired_inputs[actor_id] = [obj.name]
|
||||
else:
|
||||
paired_inputs[actor_id].append(obj.name)
|
||||
|
||||
# This is used as a small spacer
|
||||
row = layout.row(align=True)
|
||||
row.scale_y = 0.01
|
||||
row.label(text=' ')
|
||||
|
||||
# Display all paired and unpaired inputs
|
||||
for actor in animations.live_data.actors:
|
||||
show_actor(layout, actor)
|
||||
|
||||
for face in animations.live_data.faces:
|
||||
if animations.live_data.get_face_parent_id(face) == animations.live_data.get_actor_id(actor):
|
||||
split = layout.row(align=True)
|
||||
split.scale_y = row_scale
|
||||
add_indent(split)
|
||||
show_face(split, face)
|
||||
|
||||
for prop in animations.live_data.props:
|
||||
show_prop(layout, prop, scale=True)
|
||||
|
||||
|
||||
def add_indent(split, empty=False):
|
||||
row = split.row(align=True)
|
||||
row.alignment = 'LEFT'
|
||||
if empty:
|
||||
row.label(text="", icon='BLANK1')
|
||||
else:
|
||||
row.label(text="", icon_value=Icons.PAIRED.get_icon())
|
||||
|
||||
|
||||
def show_actor(layout, actor, scale=False):
|
||||
row = layout.row(align=True)
|
||||
if scale:
|
||||
row.scale_y = row_scale
|
||||
|
||||
actor_id = animations.live_data.get_actor_id(actor)
|
||||
if paired_inputs.get(actor_id):
|
||||
row.label(text=actor_id + ' --> ' + ', '.join(paired_inputs.get(actor_id)), icon_value=Icons.SUIT.get_icon())
|
||||
else:
|
||||
row.enabled = False
|
||||
row.label(text=actor_id, icon_value=Icons.SUIT.get_icon())
|
||||
|
||||
|
||||
def show_glove(layout, glove, scale=False):
|
||||
row = layout.row(align=True)
|
||||
if scale:
|
||||
row.scale_y = row_scale
|
||||
|
||||
if paired_inputs.get(glove['gloveID']):
|
||||
row.label(text=glove['gloveID'] + ' --> ' + ', '.join(paired_inputs.get(glove['gloveID'])), icon='VIEW_PAN')
|
||||
else:
|
||||
row.enabled = False
|
||||
row.label(text=glove['gloveID'], icon='VIEW_PAN')
|
||||
|
||||
|
||||
def show_face(layout, face, scale=False):
|
||||
row = layout.row(align=True)
|
||||
if scale:
|
||||
row.scale_y = row_scale
|
||||
|
||||
face_id = animations.live_data.get_face_id(face)
|
||||
if paired_inputs.get(face_id):
|
||||
row.label(text=face_id + ' --> ' + ', '.join(paired_inputs.get(face_id)), icon_value=Icons.FACE.get_icon())
|
||||
else:
|
||||
row.enabled = False
|
||||
row.label(text=face_id, icon_value=Icons.FACE.get_icon())
|
||||
|
||||
|
||||
def show_tracker(layout, tracker, scale=False):
|
||||
row = layout.row(align=True)
|
||||
if scale:
|
||||
row.scale_y = row_scale
|
||||
|
||||
if paired_inputs.get(tracker['name']):
|
||||
row.label(text=tracker['name'] + ' --> ' + ', '.join(paired_inputs.get(tracker['name'])), icon_value=Icons.VP.get_icon())
|
||||
else:
|
||||
row.enabled = False
|
||||
row.label(text=tracker['name'], icon_value=Icons.VP.get_icon())
|
||||
|
||||
|
||||
def show_prop(layout, prop, scale=False):
|
||||
row = layout.row(align=True)
|
||||
if scale:
|
||||
row.scale_y = row_scale
|
||||
|
||||
prop_id = animations.live_data.get_prop_name_raw(prop)
|
||||
if paired_inputs.get(prop_id):
|
||||
row.label(text=prop_id + ' --> ' + ', '.join(paired_inputs.get(prop_id)), icon='FILE_3D')
|
||||
else:
|
||||
row.enabled = False
|
||||
row.label(text=prop_id, icon='FILE_3D')
|
||||
@@ -0,0 +1,167 @@
|
||||
import bpy
|
||||
|
||||
from ..core import animations, animation_lists
|
||||
from ..operators.actor import InitTPose, ResetTPose
|
||||
from ..operators import detector
|
||||
|
||||
|
||||
# Create a panel in the Object category of all objects
|
||||
class ObjectsPanel(bpy.types.Panel):
|
||||
bl_label = "Rokoko Studio Live Setup"
|
||||
bl_idname = "OBJECT_PT_rsl_objects_v2"
|
||||
bl_space_type = 'PROPERTIES'
|
||||
bl_region_type = 'WINDOW'
|
||||
bl_context = "object"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
obj = context.object
|
||||
|
||||
self.draw_tracker(context, layout)
|
||||
|
||||
if obj.type == 'MESH':
|
||||
self.draw_face(context, layout)
|
||||
|
||||
elif obj.type == 'ARMATURE':
|
||||
self.draw_actor(context, layout)
|
||||
|
||||
@staticmethod
|
||||
def draw_tracker(context, layout):
|
||||
obj = context.object
|
||||
|
||||
props_string = 'Prop or Tracker' if animations.live_data.version <= 2 else 'prop'
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.label(text=f'Attach to {props_string}:')
|
||||
|
||||
if not animations.live_data.trackers and not animations.live_data.props:
|
||||
row = layout.row(align=True)
|
||||
row.label(text=f'No {props_string.lower()} data available.', icon='INFO')
|
||||
return
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.prop(context.object, 'rsl_animations_props_trackers')
|
||||
|
||||
if obj.rsl_animations_props_trackers and obj.rsl_animations_props_trackers != 'None':
|
||||
row = layout.row(align=True)
|
||||
row.prop(context.object, 'rsl_use_custom_scale')
|
||||
if obj.rsl_use_custom_scale:
|
||||
row.prop(context.object, 'rsl_custom_scene_scale', text='')
|
||||
|
||||
@staticmethod
|
||||
def draw_face(context, layout):
|
||||
obj = context.object
|
||||
|
||||
layout.separator()
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.label(text='Attach to Face:')
|
||||
|
||||
if not animations.live_data.faces:
|
||||
row = layout.row(align=True)
|
||||
row.label(text='No face data available.', icon='INFO')
|
||||
row = layout.row(align=True)
|
||||
row.scale_y = 0.1
|
||||
return
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.prop(obj, 'rsl_animations_faces')
|
||||
|
||||
if obj.rsl_animations_faces and obj.rsl_animations_faces != 'None':
|
||||
layout.separator()
|
||||
row = layout.row(align=True)
|
||||
row.label(text='Select Shapekeys:')
|
||||
row.operator(detector.DetectFaceShapes.bl_idname)
|
||||
|
||||
if not hasattr(obj.data, 'shape_keys') or not hasattr(obj.data.shape_keys, 'key_blocks'):
|
||||
row = layout.row(align=True)
|
||||
row.label(text='This mesh has no shapekeys!', icon='INFO')
|
||||
return
|
||||
|
||||
draw_import_export(layout, shapes=True)
|
||||
|
||||
for shape in animation_lists.face_shapes:
|
||||
row = layout.row(align=True)
|
||||
row.prop_search(obj, 'rsl_face_' + shape, obj.data.shape_keys, "key_blocks", text=shape)
|
||||
|
||||
@staticmethod
|
||||
def draw_actor(context, layout):
|
||||
obj = context.object
|
||||
|
||||
layout.separator()
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.label(text='Attach to Actor:')
|
||||
|
||||
if not animations.live_data.actors:
|
||||
row = layout.row(align=True)
|
||||
row.label(text='No actor data available.', icon='INFO')
|
||||
else:
|
||||
row = layout.row(align=True)
|
||||
row.prop(context.object, 'rsl_animations_actors')
|
||||
|
||||
if obj.rsl_animations_actors and obj.rsl_animations_actors != 'None':
|
||||
layout.separator()
|
||||
|
||||
split = layout.row(align=True)
|
||||
row = split.split(factor=0.16, align=True)
|
||||
row.label(text='Bones:')
|
||||
row.operator(detector.DetectActorBones.bl_idname)
|
||||
row.operator(InitTPose.bl_idname)
|
||||
row.operator(ResetTPose.bl_idname)
|
||||
|
||||
# if obj.rsl_animations_actors and obj.rsl_animations_actors != 'None':
|
||||
if not obj.get('CUSTOM') or not obj.get('CUSTOM').get('rsl_tpose_bones'):
|
||||
row = layout.row(align=True)
|
||||
row.label(text='T-Pose is not set yet!', icon='ERROR')
|
||||
|
||||
draw_import_export(layout)
|
||||
|
||||
col = layout.column()
|
||||
show_gloves = True
|
||||
for actor_bone in animation_lists.get_bones().keys():
|
||||
if not show_gloves:
|
||||
continue
|
||||
|
||||
split = col.row(align=True)
|
||||
row = split.split(factor=0.32, align=True)
|
||||
row.label(text=actor_bone + ':')
|
||||
row.prop_search(obj, 'rsl_actor_' + actor_bone, obj.pose, "bones", text='')
|
||||
|
||||
# Make a split after right toe to separate hands
|
||||
if actor_bone == 'rightToe':
|
||||
if not animations.live_data.has_gloves(animations.live_data.get_actor_by_obj(obj)): # Stop showing glove bones if they are not supported by the JSON version
|
||||
show_gloves = False
|
||||
continue
|
||||
col.separator()
|
||||
row = col.row(align=True)
|
||||
row.label(text='Gloves:', icon='VIEW_PAN')
|
||||
|
||||
if actor_bone == 'leftLittleDistal':
|
||||
col.separator()
|
||||
|
||||
|
||||
def draw_import_export(layout, shapes=False):
|
||||
layout.separator()
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.label(text='Custom Naming Schemes:')
|
||||
if shapes:
|
||||
row.operator(detector.SaveCustomShapes.bl_idname, text='Save Current Naming Scheme')
|
||||
else:
|
||||
row.operator(detector.SaveCustomBones.bl_idname, text='Save Current Naming Scheme')
|
||||
|
||||
subrow = layout.row(align=True)
|
||||
row = subrow.row(align=True)
|
||||
row.scale_y = 0.9
|
||||
row.operator(detector.ImportCustomBones.bl_idname, text='Import')
|
||||
row.operator(detector.ExportCustomBones.bl_idname, text='Export')
|
||||
row = subrow.row(align=True)
|
||||
row.scale_y = 0.9
|
||||
row.alignment = 'RIGHT'
|
||||
if shapes:
|
||||
row.operator(detector.ClearCustomShapes.bl_idname, text='', icon='X')
|
||||
else:
|
||||
row.operator(detector.ClearCustomBones.bl_idname, text='', icon='X')
|
||||
|
||||
layout.separator()
|
||||
@@ -0,0 +1,137 @@
|
||||
import bpy
|
||||
|
||||
from .main import ToolPanel
|
||||
from ..operators import retargeting, detector
|
||||
from ..core.icon_manager import Icons
|
||||
from ..core.retargeting import get_target_armature, get_source_armature
|
||||
|
||||
from bpy.types import PropertyGroup, UIList
|
||||
from bpy.props import StringProperty, BoolProperty
|
||||
|
||||
|
||||
# Retargeting panel
|
||||
class RetargetingPanel(ToolPanel, bpy.types.Panel):
|
||||
bl_idname = 'VIEW3D_PT_rsl_retargeting_v2'
|
||||
bl_label = 'Retargeting'
|
||||
bl_options = {'DEFAULT_CLOSED'}
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.use_property_split = False
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.label(text='Select the armatures:')
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.prop(context.scene, 'rsl_retargeting_armature_source', icon='ARMATURE_DATA')
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.prop(context.scene, 'rsl_retargeting_armature_target', icon='ARMATURE_DATA')
|
||||
|
||||
anim_exists = False
|
||||
for obj in bpy.data.objects:
|
||||
if obj.animation_data and obj.animation_data.action:
|
||||
anim_exists = True
|
||||
|
||||
if not anim_exists:
|
||||
row = layout.row(align=True)
|
||||
row.label(text='No animated armature found!', icon='INFO')
|
||||
return
|
||||
|
||||
if not context.scene.rsl_retargeting_armature_source or not context.scene.rsl_retargeting_armature_target:
|
||||
self.draw_import_export(layout)
|
||||
return
|
||||
|
||||
if not context.scene.rsl_retargeting_bone_list:
|
||||
row = layout.row(align=True)
|
||||
row.scale_y = 1.2
|
||||
row.operator(retargeting.BuildBoneList.bl_idname, icon_value=Icons.CALIBRATE.get_icon())
|
||||
self.draw_import_export(layout)
|
||||
return
|
||||
|
||||
subrow = layout.row(align=True)
|
||||
row = subrow.row(align=True)
|
||||
row.scale_y = 1.2
|
||||
row.operator(retargeting.BuildBoneList.bl_idname, text='Rebuild Bone List', icon_value=Icons.CALIBRATE.get_icon())
|
||||
row = subrow.row(align=True)
|
||||
row.scale_y = 1.2
|
||||
row.alignment = 'RIGHT'
|
||||
row.operator(retargeting.ClearBoneList.bl_idname, text="", icon='X')
|
||||
|
||||
layout.separator()
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.template_list("RSL_UL_BoneList", "Bone List", context.scene, "rsl_retargeting_bone_list", context.scene, "rsl_retargeting_bone_list_index", rows=1, maxrows=10)
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.operator(retargeting.AddBoneListItem.bl_idname, text="Add Custom Entry", icon='ADD')
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.prop(context.scene, 'rsl_retargeting_auto_scaling')
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.label(text='Use Pose:')
|
||||
row.prop(context.scene, 'rsl_retargeting_use_pose', expand=True)
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.scale_y = 1.4
|
||||
row.operator(retargeting.RetargetAnimation.bl_idname, icon_value=Icons.CALIBRATE.get_icon())
|
||||
|
||||
self.draw_import_export(layout)
|
||||
|
||||
def draw_import_export(self, layout):
|
||||
layout.separator()
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.label(text='Custom Naming Schemes:')
|
||||
|
||||
subrow = layout.row(align=True)
|
||||
row = subrow.row(align=True)
|
||||
row.scale_y = 0.9
|
||||
row.operator(detector.SaveCustomBonesRetargeting.bl_idname, text='Save')
|
||||
row.operator(detector.ImportCustomBones.bl_idname, text='Import')
|
||||
row.operator(detector.ExportCustomBones.bl_idname, text='Export')
|
||||
row = subrow.row(align=True)
|
||||
row.scale_y = 0.9
|
||||
row.alignment = 'RIGHT'
|
||||
row.operator(detector.ClearCustomBones.bl_idname, text='', icon='X')
|
||||
|
||||
|
||||
class BoneListItem(PropertyGroup):
|
||||
"""Properties of the bone list items"""
|
||||
bone_name_source: StringProperty(
|
||||
name="Source Bone",
|
||||
description="The source bone name",
|
||||
default="")
|
||||
|
||||
bone_name_target: StringProperty(
|
||||
name="Target Bone",
|
||||
description="The target bone name",
|
||||
default="")
|
||||
|
||||
bone_name_key: StringProperty(
|
||||
name="Auto Detection Key",
|
||||
description="The automatically detected bone key",
|
||||
default="")
|
||||
|
||||
is_custom: BoolProperty(
|
||||
description="This determines if the field is a custom one source bone one",
|
||||
default=False)
|
||||
|
||||
|
||||
class RSL_UL_BoneList(UIList):
|
||||
def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index):
|
||||
armature_target = get_target_armature()
|
||||
armature_source = get_source_armature()
|
||||
|
||||
layout = layout.split(factor=0.36, align=True)
|
||||
|
||||
# Displays source bone
|
||||
if item.is_custom:
|
||||
layout.prop_search(item, 'bone_name_source', armature_source.pose, "bones", text='')
|
||||
else:
|
||||
layout.label(text=item.bone_name_source)
|
||||
|
||||
# Displays target bone
|
||||
if armature_target:
|
||||
layout.prop_search(item, 'bone_name_target', armature_target.pose, "bones", text='')
|
||||
@@ -0,0 +1,13 @@
|
||||
import bpy
|
||||
|
||||
from .main import ToolPanel
|
||||
from .. import updater_ops
|
||||
|
||||
|
||||
class UpdaterPanel(ToolPanel, bpy.types.Panel):
|
||||
bl_idname = 'VIEW3D_PT_rsl_updater_v2'
|
||||
bl_label = 'Updater'
|
||||
bl_options = {'DEFAULT_CLOSED'}
|
||||
|
||||
def draw(self, context):
|
||||
updater_ops.draw_updater_panel(context, self.layout)
|
||||
@@ -0,0 +1,155 @@
|
||||
from bpy.types import Scene, Object
|
||||
from bpy.props import IntProperty, StringProperty, EnumProperty, BoolProperty, FloatProperty, CollectionProperty, PointerProperty
|
||||
|
||||
from .core import animation_lists, state_manager, recorder, retargeting
|
||||
from .panels import retargeting as retargeting_ui
|
||||
|
||||
|
||||
def register():
|
||||
# Receiver
|
||||
Scene.rsl_receiver_port = IntProperty(
|
||||
name='Streaming Port',
|
||||
description="The port defined in Rokoko Studio",
|
||||
default=14043,
|
||||
min=1,
|
||||
max=65535
|
||||
)
|
||||
Scene.rsl_receiver_fps = IntProperty(
|
||||
name='FPS',
|
||||
description="How often is the data received",
|
||||
default=60,
|
||||
min=1,
|
||||
max=100
|
||||
)
|
||||
Scene.rsl_scene_scaling = FloatProperty(
|
||||
name='Scene Scaling',
|
||||
description="This allows you to scale the position of props and trackers."
|
||||
"\nUseful to align their positions with armatures",
|
||||
default=1,
|
||||
precision=3,
|
||||
step=1
|
||||
)
|
||||
Scene.rsl_reset_scene_on_stop = BoolProperty(
|
||||
name='Reset Scene on Stop',
|
||||
description='This will reset the location and position of animated objects to the state of before starting the receiver',
|
||||
default=True
|
||||
)
|
||||
Scene.rsl_hide_mesh_during_play = BoolProperty(
|
||||
name='Hide Meshes during Play',
|
||||
description='This will hide all meshes that are animated by armatures'
|
||||
'\nto greatly reduce lag and increase performance.'
|
||||
'\nThis will not hide animated faces',
|
||||
default=False,
|
||||
update=state_manager.update_hidden_meshes
|
||||
)
|
||||
Scene.rsl_recording = BoolProperty(
|
||||
name='Toggle Recording',
|
||||
description='Start and stop recording of the data from Rokoko Studio',
|
||||
default=False,
|
||||
update=recorder.toggle_recording
|
||||
)
|
||||
|
||||
# Command API
|
||||
Scene.rsl_command_ip_address = StringProperty(
|
||||
name='IP Address',
|
||||
description='Input the IP address of Rokoko Studio',
|
||||
default='127.0.0.1',
|
||||
maxlen=15
|
||||
)
|
||||
Scene.rsl_command_ip_port = IntProperty(
|
||||
name='Command API Port',
|
||||
description="The port defined in Rokoko Studio",
|
||||
default=14053,
|
||||
min=1,
|
||||
max=65535
|
||||
)
|
||||
Scene.rsl_command_api_key = StringProperty(
|
||||
name='API Key',
|
||||
description='Input the API key displayed in Rokoko Studio',
|
||||
default='1234',
|
||||
maxlen=15
|
||||
)
|
||||
|
||||
# Retargeting
|
||||
Scene.rsl_retargeting_armature_source = PointerProperty(
|
||||
name='Source',
|
||||
description='Select the armature with the animation that you want to retarget',
|
||||
type=Object,
|
||||
poll=retargeting.poll_source_armatures,
|
||||
update=retargeting.clear_bone_list
|
||||
)
|
||||
Scene.rsl_retargeting_armature_target = PointerProperty(
|
||||
name='Target',
|
||||
description='Select the armature that should receive the animation',
|
||||
type=Object,
|
||||
poll=retargeting.poll_target_armatures,
|
||||
update=retargeting.clear_bone_list
|
||||
)
|
||||
Scene.rsl_retargeting_auto_scaling = BoolProperty(
|
||||
name='Auto Scale',
|
||||
description='This will scale the source armature to fit the height of the target armature.'
|
||||
'\nBoth armatures have to be in T-pose for this to work correctly',
|
||||
default=True
|
||||
)
|
||||
Scene.rsl_retargeting_use_pose = EnumProperty(
|
||||
name="Use Pose",
|
||||
description='Select which pose of the source and target armature to use to retarget the animation.'
|
||||
'\nBoth armatures should be in the same pose before retargeting',
|
||||
items=[
|
||||
("REST", "Rest", "Select this to use the rest pose during retargeting."),
|
||||
("CURRENT", "Current", "Select this to use the current pose during retargeting.")
|
||||
]
|
||||
)
|
||||
Scene.rsl_retargeting_bone_list = CollectionProperty(
|
||||
type=retargeting_ui.BoneListItem
|
||||
)
|
||||
Scene.rsl_retargeting_bone_list_index = IntProperty(
|
||||
name="Index for the retargeting bone list",
|
||||
default=0
|
||||
)
|
||||
|
||||
# Objects
|
||||
Object.rsl_animations_props_trackers = EnumProperty(
|
||||
name='Tracker or Prop',
|
||||
description='Select the prop or tracker that you want to attach this object to',
|
||||
items=animation_lists.get_props_trackers,
|
||||
update=state_manager.update_object
|
||||
)
|
||||
Object.rsl_animations_faces = EnumProperty(
|
||||
name='Face',
|
||||
description='Select the face that you want to attach this mesh to',
|
||||
items=animation_lists.get_faces,
|
||||
update=state_manager.update_face
|
||||
)
|
||||
Object.rsl_animations_actors = EnumProperty(
|
||||
name='Actor',
|
||||
description='Select the actor that you want to attach this armature to',
|
||||
items=animation_lists.get_actors,
|
||||
update=state_manager.update_actor
|
||||
)
|
||||
Object.rsl_use_custom_scale = BoolProperty(
|
||||
name='Use Custom Scale',
|
||||
description='Select this if the objects scene scaling should be overwritten',
|
||||
default=False,
|
||||
)
|
||||
Object.rsl_custom_scene_scale = FloatProperty(
|
||||
name='Custom Scene Scaling',
|
||||
description="This allows you to scale the position independently from the scene scale.",
|
||||
default=1,
|
||||
precision=3,
|
||||
step=1
|
||||
)
|
||||
|
||||
# Face shapekeys
|
||||
for shape in animation_lists.face_shapes:
|
||||
setattr(Object, 'rsl_face_' + shape, StringProperty(
|
||||
name=shape,
|
||||
description='Select the shapekey that should be animated by this shape'
|
||||
))
|
||||
|
||||
# Actor bones
|
||||
for bone in animation_lists.get_bones().keys():
|
||||
setattr(Object, 'rsl_actor_' + bone, StringProperty(
|
||||
name=bone,
|
||||
description='Select the bone that corresponds to the actors bone'
|
||||
))
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"rokoko_custom_names": true,
|
||||
"version": 1,
|
||||
"bones": {},
|
||||
"shapes": {}
|
||||
}
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,748 @@
|
||||
import os
|
||||
import bpy
|
||||
import ssl
|
||||
import time
|
||||
import json
|
||||
import urllib
|
||||
import shutil
|
||||
import pathlib
|
||||
import zipfile
|
||||
import traceback
|
||||
import addon_utils
|
||||
from threading import Thread
|
||||
from bpy.app.handlers import persistent
|
||||
|
||||
beta_branch = "beta"
|
||||
|
||||
GITHUB_URL = "https://api.github.com/repos/RokokoElectronics/rokoko-studio-live-blender/releases"
|
||||
GITHUB_URL_BETA = f"https://github.com/RokokoElectronics/rokoko-studio-live-blender/archive/{beta_branch}.zip"
|
||||
GITHUB_COMPATIBILITY_URL = "https://raw.githubusercontent.com/Rokoko/rokoko-studio-live-blender/master/version_compatibility.json"
|
||||
|
||||
downloads_dir_name = "updater_downloads"
|
||||
|
||||
path_names_to_keep = [
|
||||
downloads_dir_name,
|
||||
'resources/no_auto_ver_check.txt',
|
||||
'resources/cache',
|
||||
'resources/custom_bones',
|
||||
]
|
||||
|
||||
# Dev testing variables
|
||||
no_ver_check = False
|
||||
fake_update = False
|
||||
|
||||
# Updater variables
|
||||
version_list = []
|
||||
is_checking_for_update = False
|
||||
checked_on_startup = False
|
||||
current_version = []
|
||||
current_version_str = ''
|
||||
update_needed = False
|
||||
latest_version = None
|
||||
latest_version_str = ''
|
||||
used_updater_panel = False
|
||||
update_finished = False
|
||||
remind_me_later = False
|
||||
is_ignored_version = False
|
||||
|
||||
confirm_update_to = ''
|
||||
|
||||
show_error = ''
|
||||
|
||||
file_replacement_extension = '.renamed'
|
||||
|
||||
|
||||
main_dir = os.path.dirname(__file__)
|
||||
downloads_dir = os.path.join(main_dir, downloads_dir_name)
|
||||
resources_dir = os.path.join(main_dir, "resources")
|
||||
ignore_ver_file = os.path.join(resources_dir, "ignore_version.txt")
|
||||
no_auto_ver_check_file = os.path.join(resources_dir, "no_auto_ver_check.txt")
|
||||
delete_files_on_startup_file = os.path.join(main_dir, "delete_files_on_startup.txt")
|
||||
compatibility_file = os.path.join(main_dir, "version_compatibility.json")
|
||||
|
||||
# Compatibility checking variables
|
||||
compatibility_data = {}
|
||||
compatibility_loaded = False
|
||||
|
||||
# Get package name, important for panel in user preferences
|
||||
package_name = ''
|
||||
for mod in addon_utils.modules():
|
||||
if mod.bl_info['name'] == 'Rokoko Studio Live for Blender':
|
||||
package_name = mod.__name__
|
||||
|
||||
|
||||
class Version:
|
||||
def __init__(self, data):
|
||||
# Set version string
|
||||
version_string = data.get('tag_name').lower().replace('-', '.').replace('_', '.')
|
||||
if version_string.startswith('v.'):
|
||||
version_string = version_string[2:]
|
||||
if version_string.startswith('v'):
|
||||
version_string = version_string[1:]
|
||||
|
||||
# Set version number
|
||||
version_number = []
|
||||
for i in version_string.split('.'):
|
||||
if i.isdigit():
|
||||
version_number.append(int(i))
|
||||
|
||||
# Set version data
|
||||
self.version_string = version_string
|
||||
self.version_display_string = version_string
|
||||
self.version_number = version_number
|
||||
self.name = data.get('name')
|
||||
self.download_link = data.get('zipball_url')
|
||||
self.patch_notes = data.get('body')
|
||||
self.release_date = data.get('published_at')
|
||||
self.is_prerelease = data.get('prerelease')
|
||||
|
||||
if 'T' in data.get('published_at')[1:]:
|
||||
self.release_date = data.get('published_at').split('T')[0]
|
||||
|
||||
# If the name of the release contains "yanked", ignore it
|
||||
if 'yanked' in self.name.lower():
|
||||
return
|
||||
|
||||
if self.is_prerelease:
|
||||
self.version_display_string += ' (beta)'
|
||||
|
||||
version_list.append(self)
|
||||
|
||||
|
||||
def get_version_by_string(version_string) -> Version:
|
||||
for version in version_list:
|
||||
if version.version_string == version_string:
|
||||
return version
|
||||
|
||||
|
||||
def get_latest_version() -> Version:
|
||||
version_list_releases = [version for version in version_list if not version.is_prerelease and is_version_compatible(version.version_string)]
|
||||
return version_list_releases[0] if version_list_releases else None
|
||||
|
||||
|
||||
def load_compatibility_data():
|
||||
"""Load the version compatibility JSON from GitHub."""
|
||||
global compatibility_data, compatibility_loaded
|
||||
|
||||
if compatibility_loaded:
|
||||
return True
|
||||
|
||||
try:
|
||||
print("Fetching version compatibility data from GitHub...")
|
||||
ssl._create_default_https_context = ssl._create_unverified_context
|
||||
with urllib.request.urlopen(GITHUB_COMPATIBILITY_URL) as url:
|
||||
data = url.read().decode('utf-8')
|
||||
compatibility_data = json.loads(data)
|
||||
compatibility_loaded = True
|
||||
print("Loaded version compatibility data from GitHub")
|
||||
return True
|
||||
except urllib.error.URLError as e:
|
||||
print(f"Failed to fetch compatibility data from GitHub: {e}")
|
||||
# Try to load from local file as fallback
|
||||
try:
|
||||
if os.path.isfile(compatibility_file):
|
||||
with open(compatibility_file, 'r', encoding='utf-8') as f:
|
||||
compatibility_data = json.load(f)
|
||||
compatibility_loaded = True
|
||||
print("Loaded version compatibility data from local file as fallback")
|
||||
return True
|
||||
except Exception as local_e:
|
||||
print(f"Failed to load local compatibility file: \n{traceback.format_exc()}")
|
||||
|
||||
print("No compatibility data available, all versions will be considered compatible")
|
||||
compatibility_data = {}
|
||||
compatibility_loaded = True
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"Error loading compatibility data: \n{traceback.format_exc()}")
|
||||
compatibility_data = {}
|
||||
compatibility_loaded = True
|
||||
return False
|
||||
|
||||
|
||||
def get_compatibility_for_version(addon_version_string):
|
||||
"""Get compatibility info for a specific addon version.
|
||||
|
||||
Since the JSON only contains versions where compatibility changed,
|
||||
we need to find the highest version <= the requested version.
|
||||
"""
|
||||
if not compatibility_data:
|
||||
return None
|
||||
|
||||
# Convert version string to tuple for comparison
|
||||
def version_to_tuple(version_str):
|
||||
try:
|
||||
return tuple(int(x) for x in version_str.split('.'))
|
||||
except:
|
||||
return (0, 0, 0)
|
||||
|
||||
addon_version_tuple = version_to_tuple(addon_version_string)
|
||||
|
||||
# Find the highest version in compatibility data that is <= addon_version
|
||||
best_match = None
|
||||
best_match_tuple = (0, 0, 0)
|
||||
|
||||
for compat_version in compatibility_data.keys():
|
||||
compat_tuple = version_to_tuple(compat_version)
|
||||
if compat_tuple <= addon_version_tuple and compat_tuple > best_match_tuple:
|
||||
best_match = compat_version
|
||||
best_match_tuple = compat_tuple
|
||||
|
||||
if best_match:
|
||||
return compatibility_data[best_match]
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def refresh_compatibility_data():
|
||||
"""Force refresh of compatibility data from GitHub."""
|
||||
global compatibility_loaded
|
||||
compatibility_loaded = False
|
||||
return load_compatibility_data()
|
||||
|
||||
|
||||
def is_version_compatible(addon_version_string):
|
||||
"""Check if an addon version is compatible with the current Blender version."""
|
||||
# Load compatibility data if not already loaded
|
||||
if not load_compatibility_data():
|
||||
# If no compatibility file, assume all versions are compatible
|
||||
return True
|
||||
|
||||
# Get current Blender version as string
|
||||
blender_version = ".".join(str(x) for x in bpy.app.version)
|
||||
blender_version_tuple = bpy.app.version
|
||||
|
||||
# Get compatibility info for this addon version
|
||||
compat_info = get_compatibility_for_version(addon_version_string)
|
||||
if not compat_info:
|
||||
# No compatibility info found, assume compatible
|
||||
return True
|
||||
|
||||
# Check minimum version
|
||||
min_blender = compat_info.get('minimum_blender')
|
||||
if min_blender:
|
||||
try:
|
||||
min_tuple = tuple(int(x) for x in min_blender.split('.'))
|
||||
if blender_version_tuple < min_tuple:
|
||||
return False
|
||||
except:
|
||||
pass
|
||||
|
||||
# Check maximum version
|
||||
max_blender = compat_info.get('maximum_blender')
|
||||
if max_blender:
|
||||
try:
|
||||
max_tuple = tuple(int(x) for x in max_blender.split('.'))
|
||||
if blender_version_tuple > max_tuple:
|
||||
return False
|
||||
except:
|
||||
pass
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def check_for_update_background(check_on_startup=False):
|
||||
global is_checking_for_update, checked_on_startup
|
||||
if check_on_startup and checked_on_startup:
|
||||
# print('ALREADY CHECKED ON STARTUP')
|
||||
return
|
||||
if is_checking_for_update:
|
||||
# print('ALREADY CHECKING')
|
||||
return
|
||||
|
||||
checked_on_startup = True
|
||||
|
||||
if check_on_startup and os.path.isfile(no_auto_ver_check_file):
|
||||
print('AUTO CHECK DISABLED VIA FILE')
|
||||
return
|
||||
|
||||
is_checking_for_update = True
|
||||
|
||||
thread = Thread(target=check_for_update, args=[])
|
||||
thread.start()
|
||||
|
||||
|
||||
def check_for_update():
|
||||
print('Checking for Rokoko Studio Live update...')
|
||||
|
||||
# Refresh compatibility data from GitHub
|
||||
global compatibility_loaded
|
||||
compatibility_loaded = False # Force reload
|
||||
load_compatibility_data()
|
||||
|
||||
# Get all releases from Github
|
||||
if not get_github_releases():
|
||||
finish_update_checking(error='Could not check for updates,'
|
||||
'\ntry again later.')
|
||||
return
|
||||
|
||||
if not version_list:
|
||||
finish_update_checking(error='No plugin versions available.')
|
||||
return
|
||||
|
||||
# Check if an update is needed
|
||||
global update_needed, is_ignored_version
|
||||
update_needed = check_for_update_available()
|
||||
is_ignored_version = check_ignored_version()
|
||||
|
||||
# Update needed, show the notification popup if it wasn't checked through the UI
|
||||
if update_needed:
|
||||
print('Update found!')
|
||||
if not used_updater_panel and not is_ignored_version:
|
||||
prepare_to_show_update_notification()
|
||||
else:
|
||||
print('No update found.')
|
||||
|
||||
# Finish update checking, update the UI
|
||||
finish_update_checking()
|
||||
|
||||
|
||||
def get_github_releases():
|
||||
global version_list
|
||||
version_list = []
|
||||
|
||||
if fake_update:
|
||||
print('FAKE INSTALL!')
|
||||
|
||||
Version({
|
||||
'tag_name': '100.1',
|
||||
'name': 'Pre release!',
|
||||
'zipball_url': '',
|
||||
'body': 'Nothing new to see',
|
||||
'published_at': 'Just now!!',
|
||||
'prerelease': True
|
||||
})
|
||||
|
||||
Version({
|
||||
'tag_name': 'v-99-99',
|
||||
'name': 'v-99-99',
|
||||
'zipball_url': '',
|
||||
'body': 'Put exiting new stuff here\nOr maybe there is?',
|
||||
'published_at': 'Today',
|
||||
'prerelease': False
|
||||
})
|
||||
|
||||
Version({
|
||||
'tag_name': '12.34.56',
|
||||
'name': '12.34.56 Test Release',
|
||||
'zipball_url': '',
|
||||
'body': 'Nothing new to see',
|
||||
'published_at': 'A week ago probably',
|
||||
'prerelease': False
|
||||
})
|
||||
return True
|
||||
|
||||
try:
|
||||
ssl._create_default_https_context = ssl._create_unverified_context
|
||||
with urllib.request.urlopen(GITHUB_URL) as url:
|
||||
data = json.loads(url.read().decode())
|
||||
except urllib.error.URLError as e:
|
||||
print('URL ERROR:', e)
|
||||
return False
|
||||
|
||||
if not data:
|
||||
if type(data) == list:
|
||||
return True
|
||||
return False
|
||||
|
||||
for version_data in data:
|
||||
Version(version_data)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def check_for_update_available() -> bool:
|
||||
if not version_list:
|
||||
return False
|
||||
|
||||
global latest_version, latest_version_str
|
||||
latest_compatible_version = get_latest_version()
|
||||
|
||||
# No compatible versions found
|
||||
if not latest_compatible_version:
|
||||
return False
|
||||
|
||||
latest_version = latest_compatible_version.version_number
|
||||
latest_version_str = latest_compatible_version.version_string
|
||||
|
||||
if latest_version > current_version:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def finish_update_checking(error=''):
|
||||
global is_checking_for_update, show_error
|
||||
is_checking_for_update = False
|
||||
|
||||
# Only show error if the update panel was used before
|
||||
if used_updater_panel:
|
||||
show_error = error
|
||||
|
||||
ui_refresh()
|
||||
|
||||
|
||||
def ui_refresh():
|
||||
# A way to refresh the ui
|
||||
refreshed = False
|
||||
while not refreshed:
|
||||
if hasattr(bpy.data, 'window_managers'):
|
||||
for windowManager in bpy.data.window_managers:
|
||||
for window in windowManager.windows:
|
||||
for area in window.screen.areas:
|
||||
area.tag_redraw()
|
||||
refreshed = True
|
||||
# print('Refreshed UI')
|
||||
else:
|
||||
time.sleep(0.5)
|
||||
|
||||
|
||||
def get_update_post():
|
||||
if hasattr(bpy.app.handlers, 'scene_update_post'):
|
||||
return bpy.app.handlers.scene_update_post
|
||||
else:
|
||||
return bpy.app.handlers.depsgraph_update_post
|
||||
|
||||
|
||||
def prepare_to_show_update_notification():
|
||||
return # TODO: Implement?
|
||||
# This is necessary to show a popup directly after startup
|
||||
# You will get a nasty error otherwise
|
||||
# This will add the function to the scene_update_post and it will be executed every frame. that's why it needs to be removed again asap
|
||||
# print('PREPARE TO SHOW UI')
|
||||
if show_update_notification not in get_update_post():
|
||||
get_update_post().append(show_update_notification)
|
||||
|
||||
|
||||
@persistent
|
||||
def show_update_notification(scene): # One argument in necessary for some reason
|
||||
# print('SHOWING UI NOW!!!!')
|
||||
|
||||
# # Immediately remove this from handlers again
|
||||
if show_update_notification in get_update_post():
|
||||
get_update_post().remove(show_update_notification)
|
||||
|
||||
# Show notification popup
|
||||
# atr = UpdateNotificationPopup.bl_idname.split(".")
|
||||
# getattr(getattr(bpy.ops, atr[0]), atr[1])('INVOKE_DEFAULT')
|
||||
bpy.ops.rsl_updater.update_notification_popup('INVOKE_DEFAULT')
|
||||
|
||||
|
||||
def update_now(version=None, latest=False, beta=False):
|
||||
if fake_update:
|
||||
print('FAKE UPDATE TO VERSION:', version)
|
||||
finish_update()
|
||||
return
|
||||
if beta:
|
||||
print('UPDATE TO BETA')
|
||||
update_link = GITHUB_URL_BETA
|
||||
elif latest or not version:
|
||||
print('UPDATE TO ' + latest_version_str)
|
||||
update_link = get_latest_version().download_link
|
||||
bpy.context.scene.rsl_updater_version_list = latest_version_str
|
||||
else:
|
||||
print('UPDATE TO ' + version)
|
||||
update_link = get_version_by_string(version).download_link
|
||||
|
||||
download_file(update_link)
|
||||
|
||||
|
||||
def download_file(update_url):
|
||||
if not update_url:
|
||||
finish_update()
|
||||
return
|
||||
|
||||
# Load all the directories and files
|
||||
update_zip_file = os.path.join(downloads_dir, "rokoko-update.zip")
|
||||
|
||||
# Remove existing download folder
|
||||
if os.path.isdir(downloads_dir):
|
||||
print("DOWNLOAD FOLDER EXISTED")
|
||||
shutil.rmtree(downloads_dir)
|
||||
|
||||
# Create download folder
|
||||
pathlib.Path(downloads_dir).mkdir(exist_ok=True)
|
||||
|
||||
# Download zip
|
||||
print('DOWNLOAD FILE')
|
||||
try:
|
||||
ssl._create_default_https_context = ssl._create_unverified_context
|
||||
urllib.request.urlretrieve(update_url, update_zip_file)
|
||||
except urllib.error.URLError:
|
||||
print("FILE COULD NOT BE DOWNLOADED")
|
||||
shutil.rmtree(downloads_dir)
|
||||
finish_update(error='Could not download update.')
|
||||
return
|
||||
print('DOWNLOAD FINISHED')
|
||||
|
||||
# If zip is not downloaded, abort
|
||||
if not os.path.isfile(update_zip_file):
|
||||
print("ZIP NOT FOUND!")
|
||||
shutil.rmtree(downloads_dir)
|
||||
finish_update(error='Could not find the'
|
||||
'\ndownloaded zip.')
|
||||
return
|
||||
|
||||
# Extract the downloaded zip
|
||||
print('EXTRACTING ZIP')
|
||||
with zipfile.ZipFile(update_zip_file, "r") as zip_ref:
|
||||
zip_ref.extractall(downloads_dir)
|
||||
print('EXTRACTED')
|
||||
|
||||
# Delete the extracted zip file
|
||||
print('REMOVING ZIP FILE')
|
||||
os.remove(update_zip_file)
|
||||
|
||||
# Detect the extracted folders and files
|
||||
print('SEARCHING FOR INIT 1')
|
||||
|
||||
def search_init(path):
|
||||
print('SEARCHING IN ' + path)
|
||||
files = os.listdir(path)
|
||||
if "__init__.py" in files:
|
||||
print('FOUND')
|
||||
return path
|
||||
folders = [f for f in os.listdir(path) if os.path.isdir(os.path.join(path, f))]
|
||||
if len(folders) != 1:
|
||||
print(len(folders), 'FOLDERS DETECTED')
|
||||
return None
|
||||
print('GOING DEEPER')
|
||||
return search_init(os.path.join(path, folders[0]))
|
||||
|
||||
print('SEARCHING FOR INIT 2')
|
||||
extracted_zip_dir = search_init(downloads_dir)
|
||||
if not extracted_zip_dir:
|
||||
print("INIT NOT FOUND!")
|
||||
shutil.rmtree(downloads_dir)
|
||||
finish_update(error='Could not find Rokoko Studio'
|
||||
'\nLive in the downloaded zip.')
|
||||
return
|
||||
|
||||
# Remove old addon files
|
||||
clean_addon_dir()
|
||||
|
||||
# Move the extracted files to their correct places
|
||||
def move_files(from_dir, to_dir):
|
||||
print('MOVE FILES TO DIR:', to_dir)
|
||||
files = os.listdir(from_dir)
|
||||
for file in files:
|
||||
source_path = os.path.join(from_dir, file)
|
||||
target_path = os.path.join(to_dir, file)
|
||||
print('MOVE', source_path)
|
||||
|
||||
# If file exists, delete the target and move the new file over
|
||||
if os.path.isfile(source_path) and os.path.isfile(target_path):
|
||||
try:
|
||||
os.remove(target_path)
|
||||
except PermissionError as e:
|
||||
# If removing the target file failed, rename the new file, add its name to a file and move it over
|
||||
# It will re renamed on the next Blender startup
|
||||
print(e)
|
||||
source_path_renamed = os.path.join(from_dir, file) + file_replacement_extension
|
||||
os.rename(source_path, source_path_renamed)
|
||||
source_path = source_path_renamed
|
||||
print('File was not deleted, it will be replaced on the next startup')
|
||||
|
||||
try:
|
||||
shutil.move(source_path, to_dir)
|
||||
except shutil.Error as e:
|
||||
print('Moving still failed:', e)
|
||||
|
||||
print('REMOVED AND MOVED', file)
|
||||
|
||||
elif os.path.isdir(source_path) and os.path.isdir(target_path):
|
||||
move_files(source_path, target_path)
|
||||
|
||||
else:
|
||||
try:
|
||||
shutil.move(source_path, to_dir)
|
||||
except shutil.Error as e:
|
||||
print(e)
|
||||
print('MOVED', file)
|
||||
|
||||
move_files(extracted_zip_dir, main_dir)
|
||||
|
||||
# Delete download folder
|
||||
print('DELETE DOWNLOADS DIR')
|
||||
shutil.rmtree(downloads_dir)
|
||||
|
||||
# Finish the update
|
||||
finish_update()
|
||||
|
||||
|
||||
def finish_update(error=''):
|
||||
global update_finished, show_error
|
||||
show_error = error
|
||||
|
||||
if not error:
|
||||
update_finished = True
|
||||
|
||||
bpy.ops.rsl_updater.update_complete_panel('INVOKE_DEFAULT')
|
||||
ui_refresh()
|
||||
print("UPDATE DONE!")
|
||||
|
||||
|
||||
def clean_addon_dir():
|
||||
print("CLEAN ADDON FOLDER")
|
||||
|
||||
# Convert paths to os specific paths
|
||||
paths_to_keep = []
|
||||
for path_name in path_names_to_keep:
|
||||
path_parts = path_name.split('/')
|
||||
paths_to_keep.append(os.path.join(*path_parts))
|
||||
|
||||
for root, dirs, files in os.walk(main_dir, topdown=False):
|
||||
root_rel = os.path.relpath(root, main_dir)
|
||||
|
||||
# Ignore folders that start with a dot. If the relative path is a dot only, it means that it's the main path which shouldn't be ignored
|
||||
if root_rel.startswith('.') and root_rel != '.':
|
||||
continue
|
||||
|
||||
# Go over every file and decide whether to delete it or not
|
||||
for file in files:
|
||||
file_rel = os.path.join(root_rel, file)
|
||||
file_abs = os.path.join(root, file)
|
||||
|
||||
if file_rel.startswith('.\\') or file_rel.startswith('./'):
|
||||
file_rel = file_rel[2:]
|
||||
|
||||
# Keep the file if its exact name is on the ignore list
|
||||
if file_rel in paths_to_keep:
|
||||
continue
|
||||
|
||||
# Keep the file if part of its path is on the ignore list
|
||||
keep_file = False
|
||||
for path in paths_to_keep:
|
||||
if file_rel.startswith(path):
|
||||
keep_file = True
|
||||
break
|
||||
if keep_file:
|
||||
continue
|
||||
|
||||
# Delete the file
|
||||
try:
|
||||
os.remove(file_abs)
|
||||
print('Removed file', file_abs)
|
||||
except OSError:
|
||||
print('Failed to remove file', file_abs)
|
||||
add_file_to_delete_on_startup(file_abs)
|
||||
|
||||
# Go over every folder and decide whether to delete it or not
|
||||
for folder in dirs:
|
||||
folder_rel = os.path.join(root_rel, folder)
|
||||
folder_abs = os.path.join(root, folder)
|
||||
if folder_rel.startswith('.\\'):
|
||||
folder_rel = folder_rel[2:]
|
||||
|
||||
# Keep the folder if its exact name is on the ignore list
|
||||
if folder_rel in paths_to_keep:
|
||||
continue
|
||||
|
||||
# Delete the folder. It won't get deleted if it's not empty and that is on purpose.
|
||||
# All files in the folder should be deleted already, so keep it if there are still files in it
|
||||
try:
|
||||
os.rmdir(folder_abs)
|
||||
print('Removed folder', folder_abs)
|
||||
except OSError:
|
||||
print('Failed to remove folder', folder_abs)
|
||||
|
||||
|
||||
def add_file_to_delete_on_startup(file_path):
|
||||
# w = create and write
|
||||
# a = append to end of file
|
||||
write_type = 'a' if os.path.isfile(delete_files_on_startup_file) else 'w'
|
||||
|
||||
# Create or append "delete on startup" file
|
||||
with open(delete_files_on_startup_file, write_type, encoding="utf8") as outfile:
|
||||
outfile.write(file_path + '\n')
|
||||
|
||||
|
||||
def delete_and_rename_files_on_startup():
|
||||
if not os.path.isfile(delete_files_on_startup_file):
|
||||
return
|
||||
|
||||
with open(delete_files_on_startup_file, 'r', encoding="utf8") as outfile:
|
||||
lines = outfile.readlines()
|
||||
|
||||
# Delete the file immediately to allow it to be recreated if something fails
|
||||
os.remove(delete_files_on_startup_file)
|
||||
|
||||
for path in lines:
|
||||
if not path:
|
||||
continue
|
||||
|
||||
# Remove the line separator from the end of the path
|
||||
path = path[:-1]
|
||||
|
||||
if os.path.isfile(path):
|
||||
try:
|
||||
os.remove(path)
|
||||
print('Removed file on startup', path)
|
||||
except OSError:
|
||||
print('Failed to remove file on startup', path)
|
||||
add_file_to_delete_on_startup(path)
|
||||
continue
|
||||
|
||||
path_renamed = path + file_replacement_extension
|
||||
if os.path.isfile(path_renamed):
|
||||
os.rename(path_renamed, path)
|
||||
print('Renamed', path_renamed, 'to', path)
|
||||
|
||||
|
||||
def set_ignored_version():
|
||||
# Create resources folder
|
||||
pathlib.Path(resources_dir).mkdir(exist_ok=True)
|
||||
|
||||
# Create ignore file
|
||||
with open(ignore_ver_file, 'w', encoding="utf8") as outfile:
|
||||
outfile.write(latest_version_str)
|
||||
|
||||
# Set ignored status
|
||||
global is_ignored_version
|
||||
is_ignored_version = True
|
||||
print('IGNORE VERSION ' + latest_version_str)
|
||||
|
||||
|
||||
def check_ignored_version():
|
||||
if not os.path.isfile(ignore_ver_file):
|
||||
# print('IGNORE FILE NOT FOUND')
|
||||
return False
|
||||
|
||||
# Read ignore file
|
||||
with open(ignore_ver_file, 'r', encoding="utf8") as outfile:
|
||||
version = outfile.read()
|
||||
|
||||
# Check if the latest version matches the one in the ignore file
|
||||
if latest_version_str == version:
|
||||
print('Update ignored.')
|
||||
return True
|
||||
|
||||
# Delete ignore version file if the latest version is not the version in the file
|
||||
try:
|
||||
os.remove(ignore_ver_file)
|
||||
except OSError:
|
||||
print("FAILED TO REMOVE IGNORE VERSION FILE")
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def get_version_list(self, context):
|
||||
choices = []
|
||||
for version in version_list:
|
||||
# Only include compatible versions
|
||||
if is_version_compatible(version.version_string):
|
||||
# 1. Will be returned by context.scene
|
||||
# 2. Will be shown in lists
|
||||
# 3. will be shown in the hover description (below description)
|
||||
choices.append((version.version_string, version.version_display_string, version.version_display_string))
|
||||
else:
|
||||
# Add incompatible versions with a warning
|
||||
display_string = version.version_display_string + " (incompatible)"
|
||||
description = f"Version {version.version_string} is not compatible with Blender {'.'.join(str(x) for x in bpy.app.version)}"
|
||||
choices.append((version.version_string, display_string, description))
|
||||
|
||||
bpy.types.Object.Enum = choices
|
||||
return bpy.types.Object.Enum
|
||||
|
||||
|
||||
def get_user_preferences():
|
||||
return bpy.context.user_preferences if hasattr(bpy.context, 'user_preferences') else bpy.context.preferences
|
||||
@@ -0,0 +1,564 @@
|
||||
import bpy
|
||||
|
||||
from . import updater
|
||||
|
||||
|
||||
class CheckForUpdateButton(bpy.types.Operator):
|
||||
bl_idname = 'rsl_updater.check_for_update'
|
||||
bl_label = 'Check now for Update'
|
||||
bl_description = 'Checks if a new update is available for Rokoko Studio Live'
|
||||
bl_options = {'INTERNAL'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return not updater.is_checking_for_update
|
||||
|
||||
def execute(self, context):
|
||||
updater.used_updater_panel = True
|
||||
updater.check_for_update_background()
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class UpdateToLatestButton(bpy.types.Operator):
|
||||
bl_idname = 'rsl_updater.update_latest'
|
||||
bl_label = 'Update Now'
|
||||
bl_description = 'Updates Rokoko Studio Live to the latest compatible version'
|
||||
bl_options = {'INTERNAL'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return updater.update_needed
|
||||
|
||||
def execute(self, context):
|
||||
updater.confirm_update_to = 'latest'
|
||||
updater.used_updater_panel = True
|
||||
|
||||
bpy.ops.rsl_updater.confirm_update_panel('INVOKE_DEFAULT')
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class UpdateToSelectedButton(bpy.types.Operator):
|
||||
bl_idname = 'rsl_updater.update_selected'
|
||||
bl_label = 'Update to Selected version'
|
||||
bl_description = 'Updates Rokoko Studio Live to the selected version'
|
||||
bl_options = {'INTERNAL'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if updater.is_checking_for_update or not updater.version_list:
|
||||
return False
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
updater.confirm_update_to = context.scene.rsl_updater_version_list
|
||||
updater.used_updater_panel = True
|
||||
|
||||
bpy.ops.rsl_updater.confirm_update_panel('INVOKE_DEFAULT')
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class UpdateToBetaButton(bpy.types.Operator):
|
||||
bl_idname = 'rsl_updater.update_beta'
|
||||
bl_label = 'Update to Beta version'
|
||||
bl_description = 'Updates Rokoko Studio Live to the Beta version'
|
||||
bl_options = {'INTERNAL'}
|
||||
|
||||
def execute(self, context):
|
||||
updater.confirm_update_to = 'beta'
|
||||
updater.used_updater_panel = True
|
||||
|
||||
bpy.ops.rsl_updater.confirm_update_panel('INVOKE_DEFAULT')
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class RemindMeLaterButton(bpy.types.Operator):
|
||||
bl_idname = 'rsl_updater.remind_me_later'
|
||||
bl_label = 'Remind me later'
|
||||
bl_description = 'This hides the update notification til the next Blender restart'
|
||||
bl_options = {'INTERNAL'}
|
||||
|
||||
def execute(self, context):
|
||||
updater.remind_me_later = True
|
||||
self.report({'INFO'}, 'You will be reminded later')
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class IgnoreThisVersionButton(bpy.types.Operator):
|
||||
bl_idname = 'rsl_updater.ignore_this_version'
|
||||
bl_label = 'Ignore this version'
|
||||
bl_description = 'Ignores this version. You will be reminded again when the next version releases'
|
||||
bl_options = {'INTERNAL'}
|
||||
|
||||
def execute(self, context):
|
||||
updater.set_ignored_version()
|
||||
self.report({'INFO'}, 'Version ' + updater.latest_version_str + ' will be ignored.')
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class ShowPatchnotesPanel(bpy.types.Operator):
|
||||
bl_idname = 'rsl_updater.show_patchnotes'
|
||||
bl_label = 'Patchnotes'
|
||||
bl_description = 'Shows the patchnotes of the selected version'
|
||||
bl_options = {'INTERNAL'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if updater.is_checking_for_update or not updater.version_list:
|
||||
return False
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
return {'FINISHED'}
|
||||
|
||||
def invoke(self, context, event):
|
||||
updater.used_updater_panel = True
|
||||
dpi_value = updater.get_user_preferences().system.dpi
|
||||
return context.window_manager.invoke_props_dialog(self, width=int(dpi_value * 8.3))
|
||||
|
||||
def check(self, context):
|
||||
# Important for changing options
|
||||
return True
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
col = layout.column(align=True)
|
||||
|
||||
row = col.row(align=True)
|
||||
row.prop(context.scene, 'rsl_updater_version_list')
|
||||
|
||||
if context.scene.rsl_updater_version_list:
|
||||
version = updater.get_version_by_string(context.scene.rsl_updater_version_list)
|
||||
|
||||
col.separator()
|
||||
row = col.row(align=True)
|
||||
row.label(text=version.name, icon='SOLO_ON')
|
||||
|
||||
col.separator()
|
||||
for line in version.patch_notes.replace('**', '').split('\r\n'):
|
||||
if line.startswith("["):
|
||||
continue
|
||||
row = col.row(align=True)
|
||||
row.scale_y = 0.75
|
||||
row.label(text=line)
|
||||
|
||||
col.separator()
|
||||
row = col.row(align=True)
|
||||
row.label(text='Released: ' + version.release_date)
|
||||
|
||||
col.separator()
|
||||
|
||||
|
||||
class ConfirmUpdatePanel(bpy.types.Operator):
|
||||
bl_idname = 'rsl_updater.confirm_update_panel'
|
||||
bl_label = 'Confirm Update'
|
||||
bl_description = 'This shows you a panel in which you have to confirm your update choice'
|
||||
bl_options = {'INTERNAL'}
|
||||
|
||||
show_patchnotes = False
|
||||
|
||||
def execute(self, context):
|
||||
print('UPDATE TO ' + updater.confirm_update_to)
|
||||
if updater.confirm_update_to == 'beta':
|
||||
updater.update_now(beta=True)
|
||||
elif updater.confirm_update_to == 'latest':
|
||||
updater.update_now(latest=True)
|
||||
else:
|
||||
updater.update_now(version=updater.confirm_update_to)
|
||||
return {'FINISHED'}
|
||||
|
||||
def invoke(self, context, event):
|
||||
dpi_value = updater.get_user_preferences().system.dpi
|
||||
return context.window_manager.invoke_props_dialog(self, width=int(dpi_value * 4.2))
|
||||
|
||||
def check(self, context):
|
||||
# Important for changing options
|
||||
return True
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
col = layout.column(align=True)
|
||||
|
||||
version_str = updater.confirm_update_to
|
||||
if updater.confirm_update_to == 'latest':
|
||||
version_str = updater.latest_version_str
|
||||
elif updater.confirm_update_to == 'beta':
|
||||
version_str = 'Beta'
|
||||
|
||||
col.separator()
|
||||
row = col.row(align=True)
|
||||
row.label(text='Version: ' + version_str)
|
||||
|
||||
if updater.confirm_update_to == 'beta':
|
||||
col.separator()
|
||||
col.separator()
|
||||
row = col.row(align=True)
|
||||
row.scale_y = 0.75
|
||||
row.label(text='Warning:')
|
||||
row = col.row(align=True)
|
||||
row.scale_y = 0.75
|
||||
row.label(text=' The beta version might be unstable, some features')
|
||||
row = col.row(align=True)
|
||||
row.scale_y = 0.75
|
||||
row.label(text=' might not work correctly, or it might only be')
|
||||
row = col.row(align=True)
|
||||
row.scale_y = 0.75
|
||||
row.label(text=' compatible with the latest Blender version.')
|
||||
col.separator()
|
||||
row = col.row(align=True)
|
||||
row.scale_y = 0.75
|
||||
row.label(text=' Check the Rokoko Github repo for more details.')
|
||||
|
||||
else:
|
||||
row.operator(ShowPatchnotesPanel.bl_idname, text='Show Patchnotes')
|
||||
|
||||
col.separator()
|
||||
col.separator()
|
||||
row = col.row(align=True)
|
||||
row.scale_y = 0.65
|
||||
row.label(text='Update now:', icon='URL')
|
||||
|
||||
|
||||
class UpdateCompletePanel(bpy.types.Operator):
|
||||
bl_idname = 'rsl_updater.update_complete_panel'
|
||||
bl_label = 'Installation Report'
|
||||
bl_description = 'The update if now complete'
|
||||
bl_options = {'INTERNAL'}
|
||||
|
||||
show_patchnotes = False
|
||||
|
||||
def execute(self, context):
|
||||
return {'FINISHED'}
|
||||
|
||||
def invoke(self, context, event):
|
||||
dpi_value = updater.get_user_preferences().system.dpi
|
||||
return context.window_manager.invoke_props_dialog(self, width=int(dpi_value * 4.2))
|
||||
|
||||
def check(self, context):
|
||||
# Important for changing options
|
||||
return True
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
col = layout.column(align=True)
|
||||
|
||||
if updater.update_finished:
|
||||
row = col.row(align=True)
|
||||
row.scale_y = 0.9
|
||||
row.label(text='Rokoko Studio Live was successfully updated.', icon='FILE_TICK')
|
||||
|
||||
row = col.row(align=True)
|
||||
row.scale_y = 0.9
|
||||
row.label(text='Restart Blender to complete the update.', icon='BLANK1')
|
||||
else:
|
||||
row = col.row(align=True)
|
||||
row.scale_y = 0.9
|
||||
row.label(text='Update failed.', icon='CANCEL')
|
||||
|
||||
row = col.row(align=True)
|
||||
row.scale_y = 0.9
|
||||
row.label(text='See Updater Panel for more info.', icon='BLANK1')
|
||||
|
||||
|
||||
class UpdateNotificationPopup(bpy.types.Operator):
|
||||
bl_idname = 'rsl_updater.update_notification_popup'
|
||||
bl_label = 'Update available'
|
||||
bl_description = 'This shows you that an update is available'
|
||||
bl_options = {'INTERNAL'}
|
||||
|
||||
def execute(self, context):
|
||||
action = context.scene.rsl_update_action
|
||||
if action == 'UPDATE':
|
||||
updater.update_now(latest=True)
|
||||
elif action == 'IGNORE':
|
||||
updater.set_ignored_version()
|
||||
else:
|
||||
# Remind later aka defer
|
||||
updater.remind_me_later = True
|
||||
updater.ui_refresh()
|
||||
return {'FINISHED'}
|
||||
|
||||
def invoke(self, context, event):
|
||||
dpi_value = updater.get_user_preferences().system.dpi
|
||||
return context.window_manager.invoke_props_dialog(self, width=int(dpi_value * 4.7))
|
||||
|
||||
# def invoke(self, context, event):
|
||||
# return context.window_manager.invoke_props_dialog(self)
|
||||
|
||||
def check(self, context):
|
||||
# Important for changing options
|
||||
return True
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
col = layout.column(align=True)
|
||||
|
||||
row = col.split(factor=0.55, align=True)
|
||||
row.scale_y = 1.05
|
||||
row.label(text='Rokoko Studio Live v' + updater.latest_version_str + ' available!', icon='SOLO_ON')
|
||||
row.operator(ShowPatchnotesPanel.bl_idname, text='Show Patchnotes')
|
||||
|
||||
col.separator()
|
||||
col.separator()
|
||||
col.separator()
|
||||
row = col.row(align=True)
|
||||
row.prop(context.scene, 'rsl_update_action', expand=True)
|
||||
|
||||
|
||||
def draw_update_notification_panel(layout):
|
||||
if not updater.update_needed or updater.remind_me_later or updater.is_ignored_version:
|
||||
return
|
||||
|
||||
col = layout.column(align=True)
|
||||
|
||||
if updater.update_finished:
|
||||
col.separator()
|
||||
row = col.row(align=True)
|
||||
row.label(text='Restart Blender to complete update!', icon='ERROR')
|
||||
col.separator()
|
||||
return
|
||||
|
||||
row = col.row(align=True)
|
||||
row.scale_y = 0.75
|
||||
row.label(text='Update v' + updater.latest_version_str + ' available!', icon='SOLO_ON')
|
||||
|
||||
col.separator()
|
||||
row = col.row(align=True)
|
||||
row.scale_y = 1.3
|
||||
row.operator(UpdateToLatestButton.bl_idname, text='Update Now')
|
||||
|
||||
row = col.row(align=True)
|
||||
row.scale_y = 1
|
||||
row.operator(RemindMeLaterButton.bl_idname, text='Defer')
|
||||
row.operator(IgnoreThisVersionButton.bl_idname, text='Ignore')
|
||||
|
||||
col.separator()
|
||||
col.separator()
|
||||
col.separator()
|
||||
|
||||
|
||||
def draw_updater_panel(context, layout, user_preferences=False):
|
||||
col = layout.column(align=True)
|
||||
|
||||
scale_big = 2
|
||||
scale_small = 1.2
|
||||
|
||||
if user_preferences:
|
||||
row = col.row(align=True)
|
||||
row.scale_y = 0.8
|
||||
row.label(text='Rokoko Studio Live Updater:', icon='URL')
|
||||
col.separator()
|
||||
|
||||
if updater.update_finished:
|
||||
col.separator()
|
||||
row = col.row(align=True)
|
||||
row.scale_y = 0.75
|
||||
row.label(text='Restart Blender to', icon='ERROR')
|
||||
row = col.row(align=True)
|
||||
row.scale_y = 0.75
|
||||
row.label(text='complete the update!', icon='BLANK1')
|
||||
col.separator()
|
||||
return
|
||||
|
||||
# If the plugin didn't load correctly, don't show the current version
|
||||
if "error" in updater.current_version_str:
|
||||
row = col.row(align=True)
|
||||
row.scale_y = 0.85
|
||||
row.label(text="Failed to load the plugin. Try updating to latest or beta version:", icon='ERROR')
|
||||
col.separator()
|
||||
|
||||
if updater.show_error:
|
||||
errors = updater.show_error.split('\n')
|
||||
for i, error in enumerate(errors):
|
||||
row = col.row(align=True)
|
||||
row.scale_y = 0.85
|
||||
row.label(text=error, icon='ERROR' if i == 0 else 'BLANK1')
|
||||
col.separator()
|
||||
|
||||
if updater.is_checking_for_update:
|
||||
if not updater.used_updater_panel:
|
||||
row = col.row(align=True)
|
||||
row.scale_y = scale_big
|
||||
row.operator(CheckForUpdateButton.bl_idname, text='Checking..')
|
||||
else:
|
||||
split = col.row(align=True)
|
||||
row = split.row(align=True)
|
||||
row.scale_y = scale_big
|
||||
row.operator(CheckForUpdateButton.bl_idname, text='Checking..')
|
||||
row = split.row(align=True)
|
||||
row.alignment = 'RIGHT'
|
||||
row.scale_y = scale_big
|
||||
row.operator(CheckForUpdateButton.bl_idname, text="", icon='FILE_REFRESH')
|
||||
|
||||
elif updater.update_needed:
|
||||
split = col.row(align=True)
|
||||
row = split.row(align=True)
|
||||
row.scale_y = scale_big
|
||||
row.operator(UpdateToLatestButton.bl_idname, text='Update now to ' + updater.latest_version_str)
|
||||
row = split.row(align=True)
|
||||
row.alignment = 'RIGHT'
|
||||
row.scale_y = scale_big
|
||||
row.operator(CheckForUpdateButton.bl_idname, text="", icon='FILE_REFRESH')
|
||||
|
||||
elif not updater.used_updater_panel or not updater.version_list:
|
||||
row = col.row(align=True)
|
||||
row.scale_y = scale_big
|
||||
row.operator(CheckForUpdateButton.bl_idname, text='Check now for Update')
|
||||
|
||||
else:
|
||||
split = col.row(align=True)
|
||||
row = split.row(align=True)
|
||||
row.scale_y = scale_big
|
||||
row.operator(UpdateToLatestButton.bl_idname, text='Up to Date!')
|
||||
row = split.row(align=True)
|
||||
row.alignment = 'RIGHT'
|
||||
row.scale_y = scale_big
|
||||
row.operator(CheckForUpdateButton.bl_idname, text="", icon='FILE_REFRESH')
|
||||
|
||||
col.separator()
|
||||
col.separator()
|
||||
split = col.row(align=True)
|
||||
row = split.split(factor=0.3, align=True)
|
||||
row.scale_y = scale_small
|
||||
row.active = True if not updater.is_checking_for_update and updater.version_list else False
|
||||
row.label(text='Version:')
|
||||
row.prop(context.scene, 'rsl_updater_version_list', text='')
|
||||
row = split.row(align=True)
|
||||
row.scale_y = scale_small
|
||||
row.operator(ShowPatchnotesPanel.bl_idname, text="", icon='WORDWRAP_ON')
|
||||
|
||||
row = col.row(align=True)
|
||||
row.scale_y = scale_small
|
||||
row.active = True if not updater.is_checking_for_update and updater.version_list else False
|
||||
selected_version_compatible = updater.is_version_compatible(context.scene.rsl_updater_version_list)
|
||||
row.operator(UpdateToSelectedButton.bl_idname, text='Install Selected Version' if selected_version_compatible else 'Force Install Selected Version')
|
||||
|
||||
row = col.row(align=True)
|
||||
row.scale_y = scale_small
|
||||
row.operator(UpdateToBetaButton.bl_idname, text='Install Beta Version')
|
||||
|
||||
# If version is default, don't show the current version
|
||||
if "error" in updater.current_version_str:
|
||||
return
|
||||
|
||||
col.separator()
|
||||
row = col.row(align=True)
|
||||
row.scale_y = 0.65
|
||||
row.label(text='Add-on version: ' + updater.current_version_str)
|
||||
|
||||
# Show compatibility info
|
||||
blender_version = ".".join(str(x) for x in bpy.app.version)
|
||||
row = col.row(align=True)
|
||||
row.scale_y = 0.65
|
||||
row.label(text='Blender version: ' + blender_version)
|
||||
|
||||
|
||||
# demo bare-bones preferences
|
||||
class DemoPreferences(bpy.types.AddonPreferences):
|
||||
bl_idname = updater.package_name
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
draw_updater_panel(context, layout, user_preferences=True)
|
||||
|
||||
|
||||
to_register = [
|
||||
CheckForUpdateButton,
|
||||
UpdateToLatestButton,
|
||||
UpdateToSelectedButton,
|
||||
UpdateToBetaButton,
|
||||
RemindMeLaterButton,
|
||||
IgnoreThisVersionButton,
|
||||
ShowPatchnotesPanel,
|
||||
ConfirmUpdatePanel,
|
||||
UpdateCompletePanel,
|
||||
UpdateNotificationPopup,
|
||||
DemoPreferences,
|
||||
]
|
||||
|
||||
|
||||
registered = False
|
||||
|
||||
|
||||
def register():
|
||||
global registered
|
||||
if registered:
|
||||
return
|
||||
|
||||
# Set initial version
|
||||
current_version = []
|
||||
for i in (1, 0, 0):
|
||||
current_version.append(str(i))
|
||||
updater.current_version.append(i)
|
||||
|
||||
# Set current version string and add beta tag and increase version number
|
||||
updater.current_version_str = '.'.join(current_version)
|
||||
current_version[2] = str(int(current_version[2]) + 1)
|
||||
updater.current_version_str = '.'.join(current_version) + ".error"
|
||||
|
||||
bpy.types.Scene.rsl_updater_version_list = bpy.props.EnumProperty(
|
||||
name='Version',
|
||||
description='Select the version you want to install\n',
|
||||
items=updater.get_version_list
|
||||
)
|
||||
bpy.types.Scene.rsl_update_action = bpy.props.EnumProperty(
|
||||
name="Choose action",
|
||||
description="Action",
|
||||
items=[
|
||||
("UPDATE", "Update Now", "Updates now to the latest version"),
|
||||
("IGNORE", "Ignore this version", "This ignores this version. You will be reminded again when the next version releases"),
|
||||
("DEFER", "Remind me later", "Hides the update notification til the next Blender restart")
|
||||
]
|
||||
)
|
||||
|
||||
# Register all Updater classes
|
||||
count = 0
|
||||
for cls in to_register:
|
||||
try:
|
||||
bpy.utils.register_class(cls)
|
||||
count += 1
|
||||
except ValueError:
|
||||
pass
|
||||
# print('Registered', count, 'Rokoko Studio Live updater classes.')
|
||||
if count < len(to_register):
|
||||
print('Skipped', len(to_register) - count, 'Rokoko Studio Live updater classes.')
|
||||
|
||||
# Delete and rename files that didn't get deleted during the update process
|
||||
updater.delete_and_rename_files_on_startup()
|
||||
|
||||
print("LOADED UPDATER!")
|
||||
registered = True
|
||||
|
||||
|
||||
def update_info(bl_info, beta_branch):
|
||||
# If not beta branch, always disable fake updates and no version checks!
|
||||
if not beta_branch:
|
||||
updater.fake_update = False
|
||||
updater.no_ver_check = False
|
||||
|
||||
# Set current version
|
||||
current_version = []
|
||||
updater.current_version = []
|
||||
for i in bl_info['version']:
|
||||
current_version.append(str(i))
|
||||
updater.current_version.append(i)
|
||||
|
||||
# Set current version string (and add beta tag and increase version number if true)
|
||||
updater.current_version_str = '.'.join(current_version)
|
||||
if beta_branch:
|
||||
current_version[2] = str(int(current_version[2]) + 1)
|
||||
updater.current_version_str = '.'.join(current_version) + ".beta"
|
||||
|
||||
|
||||
def unregister():
|
||||
global registered
|
||||
registered = False
|
||||
|
||||
# Unregister all Updater classes
|
||||
for cls in reversed(to_register):
|
||||
try:
|
||||
bpy.utils.unregister_class(cls)
|
||||
except RuntimeError as e:
|
||||
print(f"Error unregistering {cls.__name__}: {e}")
|
||||
|
||||
if hasattr(bpy.types.Scene, 'rsl_updater_version_list'):
|
||||
del bpy.types.Scene.rsl_updater_version_list
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"1.0.0": {
|
||||
"minimum_blender": "2.80.75",
|
||||
"maximum_blender": "2.92.99"
|
||||
},
|
||||
"1.3.0": {
|
||||
"minimum_blender": "2.80.75",
|
||||
"maximum_blender": "2.99.99"
|
||||
},
|
||||
"1.4.0": {
|
||||
"minimum_blender": "2.80.75",
|
||||
"maximum_blender": "3.3.99"
|
||||
},
|
||||
"1.4.1": {
|
||||
"minimum_blender": "2.80.75",
|
||||
"maximum_blender": "4.0.99"
|
||||
},
|
||||
"1.4.2": {
|
||||
"minimum_blender": "2.80.75",
|
||||
"maximum_blender": null
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user