Initial commit
This commit is contained in:
commit
74d66bddf5
23
.gitignore
vendored
Normal file
23
.gitignore
vendored
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
# Generated by Cargo
|
||||||
|
# will have compiled files and executables
|
||||||
|
build/
|
||||||
|
debug/
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
|
||||||
|
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
|
||||||
|
Cargo.lock
|
||||||
|
godot-live-link-face.zip
|
||||||
|
|
||||||
|
# These are backup files generated by rustfmt
|
||||||
|
**/*.rs.bk
|
||||||
|
|
||||||
|
# MSVC Windows builds of rustc generate these, which store debugging information
|
||||||
|
*.pdb
|
||||||
|
|
||||||
|
# RustRover
|
||||||
|
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||||
|
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||||
|
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||||
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
|
#.idea/
|
11
Cargo.toml
Normal file
11
Cargo.toml
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
[package]
|
||||||
|
name = "godot-live-link-face"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type=["cdylib"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
live-link-face = { git = "https://git.zhitno.st/klink/live-link-face" }
|
||||||
|
godot = { git = "https://github.com/godot-rust/gdext", rev = "2c5f8ed" }
|
8
LICENSE-AGPLv3+NIGGER
Normal file
8
LICENSE-AGPLv3+NIGGER
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
Copyright (C) 2024 Klink
|
||||||
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, version 3.
|
||||||
|
|
||||||
|
The above copyright notice, this permission notice and the word "NIGGER" shall be included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>
|
3
LICENSE-COMMERCIAL
Normal file
3
LICENSE-COMMERCIAL
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
Copyright (C) 2024 Klink
|
||||||
|
|
||||||
|
You can buy this software to be excluded from AGPLv3+NIGGER obligations
|
17
Makefile
Normal file
17
Makefile
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
build:
|
||||||
|
cargo build --release
|
||||||
|
cargo build --target x86_64-pc-windows-gnu --release
|
||||||
|
cp godot-live-link-face.gdextension target/release
|
||||||
|
cp -R addons target/release
|
||||||
|
cp target/release/libgodot_live_link_face.so target/release/addons/godot_live_link_face
|
||||||
|
cp target/x86_64-pc-windows-gnu/release/godot_live_link_face.dll target/release/addons/godot_live_link_face
|
||||||
|
mkdir build
|
||||||
|
mkdir -p build/addons/godot_live_link_face
|
||||||
|
mv target/release/addons/* build/addons/
|
||||||
|
cp README.md build/
|
||||||
|
cp LICENSE-AGPLv3+NIGGER build/
|
||||||
|
cp LICENSE-COMMERCIAL build/
|
||||||
|
mv target/release/godot-live-link-face.gdextension build/
|
||||||
|
zip -r godot-live-link-face.zip build/*
|
||||||
|
clean:
|
||||||
|
rm -r build
|
53
README.md
Normal file
53
README.md
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
# Godot LiveLinkFace
|
||||||
|
|
||||||
|
This Godot extension allows user to connect Unreal LiveLinkFace iPhone app to the engine and control various parameters of nodes.
|
||||||
|
|
||||||
|
## Available nodes
|
||||||
|
|
||||||
|
### LiveLinkServer
|
||||||
|
This is a main node that handles incoming connections from the app.
|
||||||
|
Set "Listen IP" and "Listen Port" parameters to the ones set in the app configuration "UPD server"
|
||||||
|
If you have multiple sources of face data - add same number of LiveLinkServer nodes to the scene and set different listen port for each.
|
||||||
|
|
||||||
|
Other nodes depend on LiveLinkServer to be present in the scene
|
||||||
|
|
||||||
|
### FaceBone
|
||||||
|
This node allows to control bones of the mesh.
|
||||||
|
#### Parameters
|
||||||
|
- Read From: LiveLinkServer node to read incoming messages from
|
||||||
|
- Read Shape: Select which shape should be tracked by this node. (Ex. HeadYaw, JawOpen, etc.)
|
||||||
|
- Target Skeleton: Set the skeleton that should be controled by this node
|
||||||
|
- Target Bone: String name of the controled bone
|
||||||
|
- Clamp in Min and Clamp in Max: clamp incoming shape value to the specific range
|
||||||
|
- Clamp out Min and Clamp out Max: clamp final transform value (can be used to limit final transform range of the bone)
|
||||||
|
- Transform Axis: Which bone axis this node controls
|
||||||
|
|
||||||
|
### FaceData
|
||||||
|
This node allows to control arbitary position of Node3d
|
||||||
|
#### Parameters
|
||||||
|
- Read From: LiveLinkServer node to read incoming messages from
|
||||||
|
- Read Shape: Select which shape should be tracked by this node. (Ex. HeadYaw, JawOpen, etc.)
|
||||||
|
- Target: Set the Node3d that should be controled by this node
|
||||||
|
- Clamp in Min and Clamp in Max: clamp incoming shape value to the specific range
|
||||||
|
- Clamp out Min and Clamp out Max: clamp final transform value (can be used to limit final transform range of the bone)
|
||||||
|
- Transform Axis: Which axis this node controls
|
||||||
|
|
||||||
|
### FaceData
|
||||||
|
This node allows to control arbitary position of Node3d
|
||||||
|
#### Parameters
|
||||||
|
- Read From: LiveLinkServer node to read incoming messages from
|
||||||
|
- Read Shape: Select which shape should be tracked by this node. (Ex. HeadYaw, JawOpen, etc.)
|
||||||
|
- Target: Set the Node3d that should be controled by this node
|
||||||
|
- Clamp in Min and Clamp in Max: clamp incoming shape value to the specific range
|
||||||
|
- Clamp out Min and Clamp out Max: clamp final transform value (can be used to limit final transform range of the bone)
|
||||||
|
- Transform Axis: Which axis this node controls
|
||||||
|
|
||||||
|
### FaceProperty
|
||||||
|
This node allows to control any parameter of the node, given it takes number as a value
|
||||||
|
#### Parameters
|
||||||
|
- Read From: LiveLinkServer node to read incoming messages from
|
||||||
|
- Read Shape: Select which shape should be tracked by this node. (Ex. HeadYaw, JawOpen, etc.)
|
||||||
|
- Target Object: Set any node derived from the Object that should be controled by this node
|
||||||
|
- Target Property: String path of the controlled property
|
||||||
|
- Clamp in Min and Clamp in Max: clamp incoming shape value to the specific range
|
||||||
|
- Clamp out Min and Clamp out Max: clamp final transform value (can be used to limit final transform range of the bone)
|
231
addons/godot_live_link_face/icon.svg
Normal file
231
addons/godot_live_link_face/icon.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 24 KiB |
37
addons/godot_live_link_face/icon.svg.import
Normal file
37
addons/godot_live_link_face/icon.svg.import
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://d3yh7d8u2sf2b"
|
||||||
|
path="res://.godot/imported/icon.svg-de16b944993eced7f59f8c80c7744378.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://addons/live_link_face/icon.svg"
|
||||||
|
dest_files=["res://.godot/imported/icon.svg-de16b944993eced7f59f8c80c7744378.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=true
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
|
svg/scale=1.0
|
||||||
|
editor/scale_with_editor_scale=false
|
||||||
|
editor/convert_colors_with_editor_theme=false
|
93
flake.lock
Normal file
93
flake.lock
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"flake-utils": {
|
||||||
|
"inputs": {
|
||||||
|
"systems": "systems"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1710146030,
|
||||||
|
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"naersk": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": "nixpkgs"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1717067539,
|
||||||
|
"narHash": "sha256-oIs5EF+6VpHJRvvpVWuqCYJMMVW/6h59aYUv9lABLtY=",
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "naersk",
|
||||||
|
"rev": "fa19d8c135e776dc97f4dcca08656a0eeb28d5c0",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "naersk",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1716948383,
|
||||||
|
"narHash": "sha256-SzDKxseEcHR5KzPXLwsemyTR/kaM9whxeiJohbL04rs=",
|
||||||
|
"path": "/nix/store/qgbn0imyridkb9527v6gnv6z3jzzprb9-source",
|
||||||
|
"rev": "ad57eef4ef0659193044870c731987a6df5cf56b",
|
||||||
|
"type": "path"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"id": "nixpkgs",
|
||||||
|
"type": "indirect"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs_2": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1717112898,
|
||||||
|
"narHash": "sha256-7R2ZvOnvd9h8fDd65p0JnB7wXfUvreox3xFdYWd1BnY=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "6132b0f6e344ce2fe34fc051b72fb46e34f668e0",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixpkgs-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-utils": "flake-utils",
|
||||||
|
"naersk": "naersk",
|
||||||
|
"nixpkgs": "nixpkgs_2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"systems": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
47
flake.nix
Normal file
47
flake.nix
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
{
|
||||||
|
inputs = {
|
||||||
|
flake-utils.url = "github:numtide/flake-utils";
|
||||||
|
naersk.url = "github:nix-community/naersk";
|
||||||
|
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs = { self, flake-utils, naersk, nixpkgs }:
|
||||||
|
flake-utils.lib.eachDefaultSystem (system:
|
||||||
|
let
|
||||||
|
pkgs = (import nixpkgs) { inherit system; };
|
||||||
|
naersk' = pkgs.callPackage naersk { };
|
||||||
|
libPath = with pkgs;
|
||||||
|
lib.makeLibraryPath [
|
||||||
|
glibc
|
||||||
|
];
|
||||||
|
|
||||||
|
in rec {
|
||||||
|
# For `nix build` & `nix run`:
|
||||||
|
packages.default = naersk'.buildPackage {
|
||||||
|
src = ./.;
|
||||||
|
pname = "vtub";
|
||||||
|
nativeBuildInputs = with pkgs; [
|
||||||
|
makeWrapper
|
||||||
|
pkg-config
|
||||||
|
openssl
|
||||||
|
];
|
||||||
|
GIT_HASH = "000000000000000000000000000000";
|
||||||
|
postInstall = ''
|
||||||
|
wrapProgram "$out/bin/${packages.default.pname}" --prefix LD_LIBRARY_PATH : "${libPath}"
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
# For `nix develop`:
|
||||||
|
devShells.default = pkgs.mkShell {
|
||||||
|
nativeBuildInputs = with pkgs; [
|
||||||
|
godot_4
|
||||||
|
];
|
||||||
|
LD_LIBRARY_PATH = libPath;
|
||||||
|
env = {
|
||||||
|
VK_LAYER_PATH = "${pkgs.vulkan-validation-layers}/share/vulkan/explicit_layer.d";
|
||||||
|
RUST_BACKTRACE = 1;
|
||||||
|
RUST_LOG = "debug";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
14
godot-live-link-face.gdextension
Normal file
14
godot-live-link-face.gdextension
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
[configuration]
|
||||||
|
entry_symbol = "gdext_rust_init"
|
||||||
|
compatibility_minimum = 4.1
|
||||||
|
reloadable = true
|
||||||
|
|
||||||
|
[icons]
|
||||||
|
|
||||||
|
LiveLinkServer = "res://addons/godot_live_link_face/icon.svg"
|
||||||
|
|
||||||
|
[libraries]
|
||||||
|
linux.debug.x86_64 = "res://addons/godot_live_link_face/libgodot_live_link_face.so"
|
||||||
|
linux.release.x86_64 = "res://addons/godot_live_link_face/libgodot_live_link_face.so"
|
||||||
|
windows.debug.x86_64 = "res://addons/godot_live_link_face/godot_live_link_face.dll"
|
||||||
|
windows.release.x86_64 = "res://addons/godot_live_link_face/godot_live_link_face.dll"
|
140
src/common.rs
Normal file
140
src/common.rs
Normal file
|
@ -0,0 +1,140 @@
|
||||||
|
use godot::prelude::*;
|
||||||
|
use crate::face_data_object::FaceDataObject;
|
||||||
|
|
||||||
|
#[derive(GodotConvert, Var, Export, Debug)]
|
||||||
|
#[godot(via = GString)]
|
||||||
|
pub enum ReadBlendShape {
|
||||||
|
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,
|
||||||
|
HeadYaw,
|
||||||
|
HeadPitch,
|
||||||
|
HeadRoll,
|
||||||
|
LeftEyeYaw,
|
||||||
|
LeftEyePitch,
|
||||||
|
LeftEyeRoll,
|
||||||
|
RightEyeYaw,
|
||||||
|
RightEyePitch,
|
||||||
|
RightEyeRoll,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub fn map_range_clamped(val: f32, in_min: f32, in_max: f32, out_min: f32, out_max: f32) -> f32 {
|
||||||
|
(val - in_min) / (in_max - in_min) * (out_max - out_min) + out_min
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn map_blend_shape(value: &ReadBlendShape, face_data_object: Gd<FaceDataObject>) -> f32 {
|
||||||
|
let fdo = face_data_object.bind();
|
||||||
|
match value {
|
||||||
|
ReadBlendShape::EyeBlinkLeft => fdo.get_eye_blink_left(),
|
||||||
|
ReadBlendShape::EyeLookDownLeft => fdo.get_eye_look_down_left(),
|
||||||
|
ReadBlendShape::EyeLookInLeft => fdo.get_eye_look_in_left(),
|
||||||
|
ReadBlendShape::EyeLookOutLeft => fdo.get_eye_look_out_left(),
|
||||||
|
ReadBlendShape::EyeLookUpLeft => fdo.get_eye_look_up_left(),
|
||||||
|
ReadBlendShape::EyeSquintLeft => fdo.get_eye_squint_left(),
|
||||||
|
ReadBlendShape::EyeWideLeft => fdo.get_eye_wide_left(),
|
||||||
|
ReadBlendShape::EyeBlinkRight => fdo.get_eye_blink_right(),
|
||||||
|
ReadBlendShape::EyeLookDownRight => fdo.get_eye_look_down_right(),
|
||||||
|
ReadBlendShape::EyeLookInRight => fdo.get_eye_look_in_right(),
|
||||||
|
ReadBlendShape::EyeLookOutRight => fdo.get_eye_look_out_right(),
|
||||||
|
ReadBlendShape::EyeLookUpRight => fdo.get_eye_look_up_right(),
|
||||||
|
ReadBlendShape::EyeSquintRight => fdo.get_eye_squint_right(),
|
||||||
|
ReadBlendShape::EyeWideRight => fdo.get_eye_wide_right(),
|
||||||
|
ReadBlendShape::JawForward => fdo.get_jaw_forward(),
|
||||||
|
ReadBlendShape::JawLeft => fdo.get_jaw_left(),
|
||||||
|
ReadBlendShape::JawRight => fdo.get_jaw_right(),
|
||||||
|
ReadBlendShape::JawOpen => fdo.get_jaw_open(),
|
||||||
|
ReadBlendShape::MouthClose => fdo.get_mouth_close(),
|
||||||
|
ReadBlendShape::MouthFunnel => fdo.get_mouth_funnel(),
|
||||||
|
ReadBlendShape::MouthPucker => fdo.get_mouth_pucker(),
|
||||||
|
ReadBlendShape::MouthLeft => fdo.get_mouth_left(),
|
||||||
|
ReadBlendShape::MouthRight => fdo.get_mouth_right(),
|
||||||
|
ReadBlendShape::MouthSmileLeft => fdo.get_mouth_smile_left(),
|
||||||
|
ReadBlendShape::MouthSmileRight => fdo.get_mouth_smile_right(),
|
||||||
|
ReadBlendShape::MouthFrownLeft => fdo.get_mouth_frown_left(),
|
||||||
|
ReadBlendShape::MouthFrownRight => fdo.get_mouth_frown_right(),
|
||||||
|
ReadBlendShape::MouthDimpleLeft => fdo.get_mouth_dimple_left(),
|
||||||
|
ReadBlendShape::MouthDimpleRight => fdo.get_mouth_dimple_right(),
|
||||||
|
ReadBlendShape::MouthStretchLeft => fdo.get_mouth_stretch_left(),
|
||||||
|
ReadBlendShape::MouthStretchRight => fdo.get_mouth_stretch_right(),
|
||||||
|
ReadBlendShape::MouthRollLower => fdo.get_mouth_roll_lower(),
|
||||||
|
ReadBlendShape::MouthRollUpper => fdo.get_mouth_roll_upper(),
|
||||||
|
ReadBlendShape::MouthShrugLower => fdo.get_mouth_shrug_lower(),
|
||||||
|
ReadBlendShape::MouthShrugUpper => fdo.get_mouth_shrug_upper(),
|
||||||
|
ReadBlendShape::MouthPressLeft => fdo.get_mouth_press_left(),
|
||||||
|
ReadBlendShape::MouthPressRight => fdo.get_mouth_press_right(),
|
||||||
|
ReadBlendShape::MouthLowerDownLeft => fdo.get_mouth_lower_down_left(),
|
||||||
|
ReadBlendShape::MouthLowerDownRight => fdo.get_mouth_lower_down_right(),
|
||||||
|
ReadBlendShape::MouthUpperUpLeft => fdo.get_mouth_upper_up_left(),
|
||||||
|
ReadBlendShape::MouthUpperUpRight => fdo.get_mouth_upper_up_right(),
|
||||||
|
ReadBlendShape::BrowDownLeft => fdo.get_brow_down_left(),
|
||||||
|
ReadBlendShape::BrowDownRight => fdo.get_brow_down_right(),
|
||||||
|
ReadBlendShape::BrowInnerUp => fdo.get_brow_inner_up(),
|
||||||
|
ReadBlendShape::BrowOuterUpLeft => fdo.get_brow_outer_up_left(),
|
||||||
|
ReadBlendShape::BrowOuterUpRight => fdo.get_brow_outer_up_right(),
|
||||||
|
ReadBlendShape::CheekPuff => fdo.get_cheek_puff(),
|
||||||
|
ReadBlendShape::CheekSquintLeft => fdo.get_cheek_squint_left(),
|
||||||
|
ReadBlendShape::CheekSquintRight => fdo.get_cheek_squint_right(),
|
||||||
|
ReadBlendShape::NoseSneerLeft => fdo.get_nose_sneer_left(),
|
||||||
|
ReadBlendShape::NoseSneerRight => fdo.get_nose_sneer_right(),
|
||||||
|
ReadBlendShape::TongueOut => fdo.get_tongue_out(),
|
||||||
|
ReadBlendShape::HeadYaw => fdo.get_head_yaw(),
|
||||||
|
ReadBlendShape::HeadPitch => fdo.get_head_pitch(),
|
||||||
|
ReadBlendShape::HeadRoll => fdo.get_head_roll(),
|
||||||
|
ReadBlendShape::LeftEyeYaw => fdo.get_left_eye_yaw(),
|
||||||
|
ReadBlendShape::LeftEyePitch => fdo.get_left_eye_pitch(),
|
||||||
|
ReadBlendShape::LeftEyeRoll => fdo.get_left_eye_roll(),
|
||||||
|
ReadBlendShape::RightEyeYaw => fdo.get_right_eye_yaw(),
|
||||||
|
ReadBlendShape::RightEyePitch => fdo.get_right_eye_pitch(),
|
||||||
|
ReadBlendShape::RightEyeRoll => fdo.get_right_eye_roll(),
|
||||||
|
}
|
||||||
|
}
|
109
src/face_bone.rs
Normal file
109
src/face_bone.rs
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
use std::borrow::BorrowMut;
|
||||||
|
|
||||||
|
use godot::{engine::Skeleton3D, prelude::*};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
common::{map_blend_shape, map_range_clamped, ReadBlendShape},
|
||||||
|
face_data_object::FaceDataObject,
|
||||||
|
face_transform_rotation::TransformAxis,
|
||||||
|
live_link_server::LiveLinkServer,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(GodotClass)]
|
||||||
|
#[class(base=Node)]
|
||||||
|
pub struct FaceBone {
|
||||||
|
#[export]
|
||||||
|
read_from: Option<Gd<LiveLinkServer>>,
|
||||||
|
#[export]
|
||||||
|
read_shape: ReadBlendShape,
|
||||||
|
|
||||||
|
#[export]
|
||||||
|
target_skeleton: Option<Gd<Skeleton3D>>,
|
||||||
|
#[export]
|
||||||
|
target_bone: GString,
|
||||||
|
|
||||||
|
#[export]
|
||||||
|
clamp_in_min: f32,
|
||||||
|
#[export]
|
||||||
|
clamp_in_max: f32,
|
||||||
|
#[export]
|
||||||
|
clamp_out_min: f32,
|
||||||
|
#[export]
|
||||||
|
clamp_out_max: f32,
|
||||||
|
|
||||||
|
#[export]
|
||||||
|
transform_axis: TransformAxis,
|
||||||
|
|
||||||
|
pre_value: f32,
|
||||||
|
|
||||||
|
base: Base<Node>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[godot_api]
|
||||||
|
impl FaceBone {
|
||||||
|
#[func]
|
||||||
|
fn on_face_data(&mut self, changed: bool, face_data: Option<Gd<FaceDataObject>>) {
|
||||||
|
if changed {
|
||||||
|
let face_data = face_data.unwrap();
|
||||||
|
let shape_val = map_blend_shape(&self.read_shape, face_data);
|
||||||
|
self.pre_value = shape_val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[godot_api]
|
||||||
|
impl INode for FaceBone {
|
||||||
|
fn init(base: Base<Node>) -> Self {
|
||||||
|
Self {
|
||||||
|
read_from: None,
|
||||||
|
read_shape: ReadBlendShape::EyeBlinkLeft,
|
||||||
|
target_skeleton: None,
|
||||||
|
target_bone: GString::default(),
|
||||||
|
clamp_in_min: 0.0,
|
||||||
|
clamp_in_max: 1.0,
|
||||||
|
clamp_out_min: 0.0,
|
||||||
|
clamp_out_max: 1.0,
|
||||||
|
|
||||||
|
transform_axis: TransformAxis::X,
|
||||||
|
|
||||||
|
pre_value: 0.0,
|
||||||
|
|
||||||
|
base,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ready(&mut self) {
|
||||||
|
if let Some(mut server) = self.borrow_mut().read_from.take() {
|
||||||
|
let callable = self.base_mut().callable("on_face_data");
|
||||||
|
server.connect("face_data_updated".into(), callable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process(&mut self, _delta: f64) {
|
||||||
|
let skeleton = self.get_target_skeleton();
|
||||||
|
if let Some(mut target) = skeleton {
|
||||||
|
let bone_id = target.find_bone(self.get_target_bone());
|
||||||
|
let initial_rotation = target.get_bone_pose_rotation(bone_id);
|
||||||
|
let mut rot_euler = initial_rotation.to_euler(EulerOrder::YXZ);
|
||||||
|
let clamped = map_range_clamped(
|
||||||
|
self.pre_value,
|
||||||
|
self.clamp_in_min,
|
||||||
|
self.clamp_in_max,
|
||||||
|
self.clamp_out_min,
|
||||||
|
self.clamp_out_max,
|
||||||
|
);
|
||||||
|
match self.transform_axis {
|
||||||
|
TransformAxis::X => {
|
||||||
|
rot_euler.x = clamped;
|
||||||
|
}
|
||||||
|
TransformAxis::Y => {
|
||||||
|
rot_euler.y = clamped;
|
||||||
|
}
|
||||||
|
TransformAxis::Z => {
|
||||||
|
rot_euler.z = clamped;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
target.set_bone_pose_rotation(bone_id, Quaternion::from_euler(rot_euler).normalized());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
358
src/face_data_object.rs
Normal file
358
src/face_data_object.rs
Normal file
|
@ -0,0 +1,358 @@
|
||||||
|
use godot::prelude::*;
|
||||||
|
use live_link_face::{FaceBlendShape, LiveLinkFace};
|
||||||
|
|
||||||
|
#[derive(GodotClass)]
|
||||||
|
#[class(base=RefCounted)]
|
||||||
|
pub struct FaceDataObject {
|
||||||
|
#[var]
|
||||||
|
eye_blink_left: f32,
|
||||||
|
#[var]
|
||||||
|
eye_look_down_left: f32,
|
||||||
|
#[var]
|
||||||
|
eye_look_in_left: f32,
|
||||||
|
#[var]
|
||||||
|
eye_look_out_left: f32,
|
||||||
|
#[var]
|
||||||
|
eye_look_up_left: f32,
|
||||||
|
#[var]
|
||||||
|
eye_squint_left: f32,
|
||||||
|
#[var]
|
||||||
|
eye_wide_left: f32,
|
||||||
|
|
||||||
|
#[var]
|
||||||
|
eye_blink_right: f32,
|
||||||
|
#[var]
|
||||||
|
eye_look_down_right: f32,
|
||||||
|
#[var]
|
||||||
|
eye_look_in_right: f32,
|
||||||
|
#[var]
|
||||||
|
eye_look_out_right: f32,
|
||||||
|
#[var]
|
||||||
|
eye_look_up_right: f32,
|
||||||
|
#[var]
|
||||||
|
eye_squint_right: f32,
|
||||||
|
#[var]
|
||||||
|
eye_wide_right: f32,
|
||||||
|
|
||||||
|
#[var]
|
||||||
|
jaw_forward: f32,
|
||||||
|
#[var]
|
||||||
|
jaw_left: f32,
|
||||||
|
#[var]
|
||||||
|
jaw_right: f32,
|
||||||
|
#[var]
|
||||||
|
jaw_open: f32,
|
||||||
|
|
||||||
|
#[var]
|
||||||
|
mouth_close: f32,
|
||||||
|
#[var]
|
||||||
|
mouth_funnel: f32,
|
||||||
|
#[var]
|
||||||
|
mouth_pucker: f32,
|
||||||
|
#[var]
|
||||||
|
mouth_left: f32,
|
||||||
|
#[var]
|
||||||
|
mouth_right: f32,
|
||||||
|
#[var]
|
||||||
|
mouth_smile_left: f32,
|
||||||
|
#[var]
|
||||||
|
mouth_smile_right: f32,
|
||||||
|
#[var]
|
||||||
|
mouth_frown_left: f32,
|
||||||
|
#[var]
|
||||||
|
mouth_frown_right: f32,
|
||||||
|
#[var]
|
||||||
|
mouth_dimple_left: f32,
|
||||||
|
#[var]
|
||||||
|
mouth_dimple_right: f32,
|
||||||
|
#[var]
|
||||||
|
mouth_stretch_left: f32,
|
||||||
|
#[var]
|
||||||
|
mouth_stretch_right: f32,
|
||||||
|
#[var]
|
||||||
|
mouth_roll_lower: f32,
|
||||||
|
#[var]
|
||||||
|
mouth_roll_upper: f32,
|
||||||
|
#[var]
|
||||||
|
mouth_shrug_lower: f32,
|
||||||
|
#[var]
|
||||||
|
mouth_shrug_upper: f32,
|
||||||
|
#[var]
|
||||||
|
mouth_press_left: f32,
|
||||||
|
#[var]
|
||||||
|
mouth_press_right: f32,
|
||||||
|
#[var]
|
||||||
|
mouth_lower_down_left: f32,
|
||||||
|
#[var]
|
||||||
|
mouth_lower_down_right: f32,
|
||||||
|
#[var]
|
||||||
|
mouth_upper_up_left: f32,
|
||||||
|
#[var]
|
||||||
|
mouth_upper_up_right: f32,
|
||||||
|
|
||||||
|
#[var]
|
||||||
|
brow_down_left: f32,
|
||||||
|
#[var]
|
||||||
|
brow_down_right: f32,
|
||||||
|
#[var]
|
||||||
|
brow_inner_up: f32,
|
||||||
|
#[var]
|
||||||
|
brow_outer_up_left: f32,
|
||||||
|
#[var]
|
||||||
|
brow_outer_up_right: f32,
|
||||||
|
|
||||||
|
#[var]
|
||||||
|
cheek_puff: f32,
|
||||||
|
#[var]
|
||||||
|
cheek_squint_left: f32,
|
||||||
|
#[var]
|
||||||
|
cheek_squint_right: f32,
|
||||||
|
|
||||||
|
#[var]
|
||||||
|
nose_sneer_left: f32,
|
||||||
|
#[var]
|
||||||
|
nose_sneer_right: f32,
|
||||||
|
|
||||||
|
#[var]
|
||||||
|
tongue_out: f32,
|
||||||
|
|
||||||
|
#[var]
|
||||||
|
head_yaw: f32,
|
||||||
|
#[var]
|
||||||
|
head_pitch: f32,
|
||||||
|
#[var]
|
||||||
|
head_roll: f32,
|
||||||
|
|
||||||
|
#[var]
|
||||||
|
left_eye_yaw: f32,
|
||||||
|
#[var]
|
||||||
|
left_eye_pitch: f32,
|
||||||
|
#[var]
|
||||||
|
left_eye_roll: f32,
|
||||||
|
|
||||||
|
#[var]
|
||||||
|
right_eye_yaw: f32,
|
||||||
|
#[var]
|
||||||
|
right_eye_pitch: f32,
|
||||||
|
#[var]
|
||||||
|
right_eye_roll: f32,
|
||||||
|
|
||||||
|
base: Base<RefCounted>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_face_data(face_data_object: &mut Gd<FaceDataObject>, face_data: &LiveLinkFace) {
|
||||||
|
let mut face_data_object = face_data_object.bind_mut();
|
||||||
|
|
||||||
|
face_data_object.eye_blink_left = face_data.get_blendshape(FaceBlendShape::EyeBlinkLeft);
|
||||||
|
|
||||||
|
face_data_object.eye_look_down_left = face_data.get_blendshape(FaceBlendShape::EyeLookDownLeft);
|
||||||
|
|
||||||
|
face_data_object.eye_look_in_left = face_data.get_blendshape(FaceBlendShape::EyeLookInLeft);
|
||||||
|
|
||||||
|
face_data_object.eye_look_out_left = face_data.get_blendshape(FaceBlendShape::EyeLookOutLeft);
|
||||||
|
|
||||||
|
face_data_object.eye_look_up_left = face_data.get_blendshape(FaceBlendShape::EyeLookUpLeft);
|
||||||
|
|
||||||
|
face_data_object.eye_squint_left = face_data.get_blendshape(FaceBlendShape::EyeSquintLeft);
|
||||||
|
|
||||||
|
face_data_object.eye_wide_left = face_data.get_blendshape(FaceBlendShape::EyeWideLeft);
|
||||||
|
|
||||||
|
face_data_object.eye_blink_right = face_data.get_blendshape(FaceBlendShape::EyeBlinkRight);
|
||||||
|
|
||||||
|
face_data_object.eye_look_down_right =
|
||||||
|
face_data.get_blendshape(FaceBlendShape::EyeLookDownRight);
|
||||||
|
|
||||||
|
face_data_object.eye_look_in_right = face_data.get_blendshape(FaceBlendShape::EyeLookInRight);
|
||||||
|
|
||||||
|
face_data_object.eye_look_out_right = face_data.get_blendshape(FaceBlendShape::EyeLookOutRight);
|
||||||
|
|
||||||
|
face_data_object.eye_look_up_right = face_data.get_blendshape(FaceBlendShape::EyeLookUpRight);
|
||||||
|
|
||||||
|
face_data_object.eye_squint_right = face_data.get_blendshape(FaceBlendShape::EyeSquintRight);
|
||||||
|
|
||||||
|
face_data_object.eye_wide_right = face_data.get_blendshape(FaceBlendShape::EyeWideRight);
|
||||||
|
|
||||||
|
face_data_object.jaw_forward = face_data.get_blendshape(FaceBlendShape::JawForward);
|
||||||
|
|
||||||
|
face_data_object.jaw_left = face_data.get_blendshape(FaceBlendShape::JawLeft);
|
||||||
|
|
||||||
|
face_data_object.jaw_right = face_data.get_blendshape(FaceBlendShape::JawRight);
|
||||||
|
|
||||||
|
face_data_object.jaw_open = face_data.get_blendshape(FaceBlendShape::JawOpen);
|
||||||
|
|
||||||
|
face_data_object.mouth_close = face_data.get_blendshape(FaceBlendShape::MouthClose);
|
||||||
|
|
||||||
|
face_data_object.mouth_funnel = face_data.get_blendshape(FaceBlendShape::MouthFunnel);
|
||||||
|
|
||||||
|
face_data_object.mouth_pucker = face_data.get_blendshape(FaceBlendShape::MouthPucker);
|
||||||
|
|
||||||
|
face_data_object.mouth_left = face_data.get_blendshape(FaceBlendShape::MouthLeft);
|
||||||
|
|
||||||
|
face_data_object.mouth_right = face_data.get_blendshape(FaceBlendShape::MouthRight);
|
||||||
|
|
||||||
|
face_data_object.mouth_smile_left = face_data.get_blendshape(FaceBlendShape::MouthSmileLeft);
|
||||||
|
|
||||||
|
face_data_object.mouth_smile_right = face_data.get_blendshape(FaceBlendShape::MouthSmileRight);
|
||||||
|
|
||||||
|
face_data_object.mouth_frown_left = face_data.get_blendshape(FaceBlendShape::MouthFrownLeft);
|
||||||
|
|
||||||
|
face_data_object.mouth_frown_right = face_data.get_blendshape(FaceBlendShape::MouthFrownRight);
|
||||||
|
|
||||||
|
face_data_object.mouth_dimple_left = face_data.get_blendshape(FaceBlendShape::MouthDimpleLeft);
|
||||||
|
|
||||||
|
face_data_object.mouth_dimple_right =
|
||||||
|
face_data.get_blendshape(FaceBlendShape::MouthDimpleRight);
|
||||||
|
|
||||||
|
face_data_object.mouth_stretch_left =
|
||||||
|
face_data.get_blendshape(FaceBlendShape::MouthStretchLeft);
|
||||||
|
|
||||||
|
face_data_object.mouth_stretch_right =
|
||||||
|
face_data.get_blendshape(FaceBlendShape::MouthStretchRight);
|
||||||
|
|
||||||
|
face_data_object.mouth_roll_lower = face_data.get_blendshape(FaceBlendShape::MouthRollLower);
|
||||||
|
|
||||||
|
face_data_object.mouth_roll_upper = face_data.get_blendshape(FaceBlendShape::MouthRollUpper);
|
||||||
|
|
||||||
|
face_data_object.mouth_shrug_lower = face_data.get_blendshape(FaceBlendShape::MouthShrugLower);
|
||||||
|
|
||||||
|
face_data_object.mouth_shrug_upper = face_data.get_blendshape(FaceBlendShape::MouthShrugUpper);
|
||||||
|
|
||||||
|
face_data_object.mouth_press_left = face_data.get_blendshape(FaceBlendShape::MouthPressLeft);
|
||||||
|
|
||||||
|
face_data_object.mouth_press_right = face_data.get_blendshape(FaceBlendShape::MouthPressRight);
|
||||||
|
|
||||||
|
face_data_object.mouth_lower_down_left =
|
||||||
|
face_data.get_blendshape(FaceBlendShape::MouthLowerDownLeft);
|
||||||
|
|
||||||
|
face_data_object.mouth_lower_down_right =
|
||||||
|
face_data.get_blendshape(FaceBlendShape::MouthLowerDownRight);
|
||||||
|
|
||||||
|
face_data_object.mouth_upper_up_left =
|
||||||
|
face_data.get_blendshape(FaceBlendShape::MouthUpperUpLeft);
|
||||||
|
|
||||||
|
face_data_object.mouth_upper_up_right =
|
||||||
|
face_data.get_blendshape(FaceBlendShape::MouthUpperUpRight);
|
||||||
|
|
||||||
|
face_data_object.brow_down_left = face_data.get_blendshape(FaceBlendShape::BrowDownLeft);
|
||||||
|
|
||||||
|
face_data_object.brow_down_right = face_data.get_blendshape(FaceBlendShape::BrowDownRight);
|
||||||
|
|
||||||
|
face_data_object.brow_inner_up = face_data.get_blendshape(FaceBlendShape::BrowInnerUp);
|
||||||
|
|
||||||
|
face_data_object.brow_outer_up_left = face_data.get_blendshape(FaceBlendShape::BrowOuterUpLeft);
|
||||||
|
|
||||||
|
face_data_object.brow_outer_up_right =
|
||||||
|
face_data.get_blendshape(FaceBlendShape::BrowOuterUpRight);
|
||||||
|
|
||||||
|
face_data_object.cheek_puff = face_data.get_blendshape(FaceBlendShape::CheekPuff);
|
||||||
|
|
||||||
|
face_data_object.cheek_squint_left = face_data.get_blendshape(FaceBlendShape::CheekSquintLeft);
|
||||||
|
|
||||||
|
face_data_object.cheek_squint_right =
|
||||||
|
face_data.get_blendshape(FaceBlendShape::CheekSquintRight);
|
||||||
|
|
||||||
|
face_data_object.nose_sneer_left = face_data.get_blendshape(FaceBlendShape::NoseSneerLeft);
|
||||||
|
|
||||||
|
face_data_object.nose_sneer_right = face_data.get_blendshape(FaceBlendShape::NoseSneerRight);
|
||||||
|
|
||||||
|
face_data_object.tongue_out = face_data.get_blendshape(FaceBlendShape::TongueOut);
|
||||||
|
|
||||||
|
face_data_object.head_yaw = face_data.get_blendshape(FaceBlendShape::HeadYaw);
|
||||||
|
|
||||||
|
face_data_object.head_pitch = face_data.get_blendshape(FaceBlendShape::HeadPitch);
|
||||||
|
|
||||||
|
face_data_object.head_roll = face_data.get_blendshape(FaceBlendShape::HeadRoll);
|
||||||
|
|
||||||
|
face_data_object.left_eye_yaw = face_data.get_blendshape(FaceBlendShape::LeftEyeYaw);
|
||||||
|
|
||||||
|
face_data_object.left_eye_pitch = face_data.get_blendshape(FaceBlendShape::LeftEyePitch);
|
||||||
|
|
||||||
|
face_data_object.left_eye_roll = face_data.get_blendshape(FaceBlendShape::LeftEyeRoll);
|
||||||
|
|
||||||
|
face_data_object.right_eye_yaw = face_data.get_blendshape(FaceBlendShape::RightEyeYaw);
|
||||||
|
|
||||||
|
face_data_object.right_eye_pitch = face_data.get_blendshape(FaceBlendShape::RightEyePitch);
|
||||||
|
|
||||||
|
face_data_object.right_eye_roll = face_data.get_blendshape(FaceBlendShape::RightEyeRoll);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[godot_api]
|
||||||
|
impl IRefCounted for FaceDataObject {
|
||||||
|
fn init(base: Base<RefCounted>) -> Self {
|
||||||
|
Self {
|
||||||
|
eye_blink_left: 0.0,
|
||||||
|
eye_look_down_left: 0.0,
|
||||||
|
eye_look_in_left: 0.0,
|
||||||
|
eye_look_out_left: 0.0,
|
||||||
|
eye_look_up_left: 0.0,
|
||||||
|
eye_squint_left: 0.0,
|
||||||
|
eye_wide_left: 0.0,
|
||||||
|
|
||||||
|
eye_blink_right: 0.0,
|
||||||
|
eye_look_down_right: 0.0,
|
||||||
|
eye_look_in_right: 0.0,
|
||||||
|
eye_look_out_right: 0.0,
|
||||||
|
eye_look_up_right: 0.0,
|
||||||
|
eye_squint_right: 0.0,
|
||||||
|
eye_wide_right: 0.0,
|
||||||
|
|
||||||
|
jaw_forward: 0.0,
|
||||||
|
jaw_left: 0.0,
|
||||||
|
jaw_right: 0.0,
|
||||||
|
jaw_open: 0.0,
|
||||||
|
|
||||||
|
mouth_close: 0.0,
|
||||||
|
mouth_funnel: 0.0,
|
||||||
|
mouth_pucker: 0.0,
|
||||||
|
mouth_left: 0.0,
|
||||||
|
mouth_right: 0.0,
|
||||||
|
mouth_smile_left: 0.0,
|
||||||
|
mouth_smile_right: 0.0,
|
||||||
|
mouth_frown_left: 0.0,
|
||||||
|
mouth_frown_right: 0.0,
|
||||||
|
mouth_dimple_left: 0.0,
|
||||||
|
mouth_dimple_right: 0.0,
|
||||||
|
mouth_stretch_left: 0.0,
|
||||||
|
mouth_stretch_right: 0.0,
|
||||||
|
mouth_roll_lower: 0.0,
|
||||||
|
mouth_roll_upper: 0.0,
|
||||||
|
mouth_shrug_lower: 0.0,
|
||||||
|
mouth_shrug_upper: 0.0,
|
||||||
|
mouth_press_left: 0.0,
|
||||||
|
mouth_press_right: 0.0,
|
||||||
|
mouth_lower_down_left: 0.0,
|
||||||
|
mouth_lower_down_right: 0.0,
|
||||||
|
mouth_upper_up_left: 0.0,
|
||||||
|
mouth_upper_up_right: 0.0,
|
||||||
|
|
||||||
|
brow_down_left: 0.0,
|
||||||
|
brow_down_right: 0.0,
|
||||||
|
brow_inner_up: 0.0,
|
||||||
|
brow_outer_up_left: 0.0,
|
||||||
|
brow_outer_up_right: 0.0,
|
||||||
|
|
||||||
|
cheek_puff: 0.0,
|
||||||
|
cheek_squint_left: 0.0,
|
||||||
|
cheek_squint_right: 0.0,
|
||||||
|
|
||||||
|
nose_sneer_left: 0.0,
|
||||||
|
nose_sneer_right: 0.0,
|
||||||
|
|
||||||
|
tongue_out: 0.0,
|
||||||
|
|
||||||
|
head_yaw: 0.0,
|
||||||
|
head_pitch: 0.0,
|
||||||
|
head_roll: 0.0,
|
||||||
|
|
||||||
|
left_eye_yaw: 0.0,
|
||||||
|
left_eye_pitch: 0.0,
|
||||||
|
left_eye_roll: 0.0,
|
||||||
|
|
||||||
|
right_eye_yaw: 0.0,
|
||||||
|
right_eye_pitch: 0.0,
|
||||||
|
right_eye_roll: 0.0,
|
||||||
|
|
||||||
|
base,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
91
src/face_property.rs
Normal file
91
src/face_property.rs
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
use std::borrow::BorrowMut;
|
||||||
|
|
||||||
|
use godot::prelude::*;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
common::{map_blend_shape, map_range_clamped, ReadBlendShape},
|
||||||
|
face_data_object::FaceDataObject,
|
||||||
|
live_link_server::LiveLinkServer,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(GodotClass)]
|
||||||
|
#[class(base=Node)]
|
||||||
|
pub struct FaceProperty {
|
||||||
|
#[export]
|
||||||
|
read_from: Option<Gd<LiveLinkServer>>,
|
||||||
|
#[export]
|
||||||
|
read_shape: ReadBlendShape,
|
||||||
|
|
||||||
|
#[export]
|
||||||
|
target_object: Option<Gd<Node>>,
|
||||||
|
#[export]
|
||||||
|
target_property: StringName,
|
||||||
|
|
||||||
|
#[export]
|
||||||
|
clamp_in_min: f32,
|
||||||
|
#[export]
|
||||||
|
clamp_in_max: f32,
|
||||||
|
#[export]
|
||||||
|
clamp_out_min: f32,
|
||||||
|
#[export]
|
||||||
|
clamp_out_max: f32,
|
||||||
|
|
||||||
|
pre_value: f32,
|
||||||
|
|
||||||
|
base: Base<Node>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[godot_api]
|
||||||
|
impl FaceProperty {
|
||||||
|
#[func]
|
||||||
|
fn on_face_data(&mut self, changed: bool, face_data: Option<Gd<FaceDataObject>>) {
|
||||||
|
if changed {
|
||||||
|
let face_data = face_data.unwrap();
|
||||||
|
let shape_val = map_blend_shape(&self.read_shape, face_data);
|
||||||
|
self.pre_value = shape_val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[godot_api]
|
||||||
|
impl INode for FaceProperty {
|
||||||
|
fn init(base: Base<Node>) -> Self {
|
||||||
|
Self {
|
||||||
|
read_from: None,
|
||||||
|
read_shape: ReadBlendShape::EyeBlinkLeft,
|
||||||
|
target_object: None,
|
||||||
|
target_property: StringName::default(),
|
||||||
|
clamp_in_min: 0.0,
|
||||||
|
clamp_in_max: 1.0,
|
||||||
|
clamp_out_min: 0.0,
|
||||||
|
clamp_out_max: 1.0,
|
||||||
|
|
||||||
|
pre_value: 0.0,
|
||||||
|
|
||||||
|
base,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ready(&mut self) {
|
||||||
|
if let Some(mut server) = self.borrow_mut().read_from.take() {
|
||||||
|
let callable = self.base_mut().callable("on_face_data");
|
||||||
|
server.connect("face_data_updated".into(), callable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process(&mut self, _delta: f64) {
|
||||||
|
let object = self.get_target_object();
|
||||||
|
let property = self.get_target_property();
|
||||||
|
|
||||||
|
if let Some(mut target) = object {
|
||||||
|
let value = map_range_clamped(
|
||||||
|
self.pre_value,
|
||||||
|
self.clamp_in_min,
|
||||||
|
self.clamp_in_max,
|
||||||
|
self.clamp_out_min,
|
||||||
|
self.clamp_out_max,
|
||||||
|
);
|
||||||
|
target.set(property.clone(), Variant::from(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
112
src/face_transform_rotation.rs
Normal file
112
src/face_transform_rotation.rs
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
use std::borrow::BorrowMut;
|
||||||
|
|
||||||
|
use godot::prelude::*;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
common::{map_blend_shape, map_range_clamped, ReadBlendShape},
|
||||||
|
face_data_object::FaceDataObject,
|
||||||
|
live_link_server::LiveLinkServer,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(GodotConvert, Var, Export, Debug)]
|
||||||
|
#[godot(via = GString)]
|
||||||
|
pub enum TransformAxis {
|
||||||
|
X,
|
||||||
|
Y,
|
||||||
|
Z,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(GodotClass)]
|
||||||
|
#[class(base=Node)]
|
||||||
|
pub struct FaceData {
|
||||||
|
#[export]
|
||||||
|
read_from: Option<Gd<LiveLinkServer>>,
|
||||||
|
#[export]
|
||||||
|
read_shape: ReadBlendShape,
|
||||||
|
|
||||||
|
#[export]
|
||||||
|
target: Option<Gd<Node3D>>,
|
||||||
|
|
||||||
|
#[export]
|
||||||
|
clamp_in_min: f32,
|
||||||
|
#[export]
|
||||||
|
clamp_in_max: f32,
|
||||||
|
#[export]
|
||||||
|
clamp_out_min: f32,
|
||||||
|
#[export]
|
||||||
|
clamp_out_max: f32,
|
||||||
|
|
||||||
|
#[export]
|
||||||
|
transform_axis: TransformAxis,
|
||||||
|
|
||||||
|
pre_value: f32,
|
||||||
|
|
||||||
|
base: Base<Node>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[godot_api]
|
||||||
|
impl FaceData {
|
||||||
|
#[func]
|
||||||
|
fn on_face_data(&mut self, changed: bool, face_data: Option<Gd<FaceDataObject>>) {
|
||||||
|
if changed {
|
||||||
|
let face_data = face_data.unwrap();
|
||||||
|
let shape_val = map_blend_shape(&self.read_shape, face_data);
|
||||||
|
self.pre_value = shape_val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[godot_api]
|
||||||
|
impl INode for FaceData {
|
||||||
|
fn init(base: Base<Node>) -> Self {
|
||||||
|
Self {
|
||||||
|
read_from: None,
|
||||||
|
read_shape: ReadBlendShape::EyeBlinkLeft,
|
||||||
|
target: None,
|
||||||
|
clamp_in_min: 0.0,
|
||||||
|
clamp_in_max: 1.0,
|
||||||
|
clamp_out_min: 0.0,
|
||||||
|
clamp_out_max: 1.0,
|
||||||
|
|
||||||
|
transform_axis: TransformAxis::X,
|
||||||
|
|
||||||
|
pre_value: 0.0,
|
||||||
|
|
||||||
|
base,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ready(&mut self) {
|
||||||
|
if let Some(mut server) = self.borrow_mut().read_from.take() {
|
||||||
|
let callable = self.base_mut().callable("on_face_data");
|
||||||
|
server.connect("face_data_updated".into(), callable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process(&mut self, _delta: f64) {
|
||||||
|
if let Some(target) = &mut self.target {
|
||||||
|
let mut rotation = target.get_rotation_degrees();
|
||||||
|
let clamped = map_range_clamped(
|
||||||
|
self.pre_value,
|
||||||
|
self.clamp_in_min,
|
||||||
|
self.clamp_in_max,
|
||||||
|
self.clamp_out_min,
|
||||||
|
self.clamp_out_max,
|
||||||
|
);
|
||||||
|
|
||||||
|
match self.transform_axis {
|
||||||
|
TransformAxis::X => {
|
||||||
|
rotation.x = clamped;
|
||||||
|
}
|
||||||
|
TransformAxis::Y => {
|
||||||
|
rotation.y = clamped;
|
||||||
|
}
|
||||||
|
TransformAxis::Z => {
|
||||||
|
rotation.z = clamped;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
target.set_rotation_degrees(rotation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
14
src/lib.rs
Normal file
14
src/lib.rs
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
// use godot::engine::global::Error;
|
||||||
|
use godot::prelude::*;
|
||||||
|
|
||||||
|
struct LiveLinkExtension;
|
||||||
|
|
||||||
|
#[gdextension]
|
||||||
|
unsafe impl ExtensionLibrary for LiveLinkExtension {}
|
||||||
|
|
||||||
|
pub mod common;
|
||||||
|
pub mod live_link_server;
|
||||||
|
pub mod face_transform_rotation;
|
||||||
|
pub mod face_data_object;
|
||||||
|
pub mod face_bone;
|
||||||
|
pub mod face_property;
|
91
src/live_link_server.rs
Normal file
91
src/live_link_server.rs
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
use std::borrow::BorrowMut;
|
||||||
|
|
||||||
|
use godot::engine::global::Error;
|
||||||
|
use godot::engine::{Node, UdpServer};
|
||||||
|
use godot::obj::WithBaseField;
|
||||||
|
use godot::prelude::*;
|
||||||
|
use live_link_face::LiveLinkFace;
|
||||||
|
|
||||||
|
use crate::face_data_object::{read_face_data, FaceDataObject};
|
||||||
|
|
||||||
|
#[derive(GodotClass)]
|
||||||
|
#[class(base=Node)]
|
||||||
|
pub struct LiveLinkServer {
|
||||||
|
#[export]
|
||||||
|
listen_ip: GString,
|
||||||
|
#[export]
|
||||||
|
listen_port: u16,
|
||||||
|
udp_server: Gd<UdpServer>,
|
||||||
|
|
||||||
|
pub face_data: Option<LiveLinkFace>,
|
||||||
|
|
||||||
|
base: Base<Node>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[godot_api]
|
||||||
|
impl LiveLinkServer {
|
||||||
|
#[signal]
|
||||||
|
fn face_data_updated(changed: bool, fd: Option<Gd<FaceDataObject>>);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[godot_api]
|
||||||
|
impl INode for LiveLinkServer {
|
||||||
|
fn init(base: Base<Node>) -> Self {
|
||||||
|
let server = UdpServer::new_gd();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
listen_ip: "100.64.0.2".into(),
|
||||||
|
listen_port: 3011,
|
||||||
|
udp_server: server,
|
||||||
|
face_data: None,
|
||||||
|
base,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ready(&mut self) {
|
||||||
|
let err = self
|
||||||
|
.udp_server
|
||||||
|
.listen_ex(self.listen_port)
|
||||||
|
.bind_address(self.listen_ip.clone())
|
||||||
|
.done();
|
||||||
|
|
||||||
|
match err {
|
||||||
|
Error::OK => {}
|
||||||
|
_ => godot_error!("UdpServer listen error"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn enter_tree(&mut self) {}
|
||||||
|
|
||||||
|
fn process(&mut self, _delta: f64) {
|
||||||
|
self.udp_server.poll();
|
||||||
|
|
||||||
|
if self.udp_server.is_connection_available() {
|
||||||
|
if let Some(mut peer) = self.udp_server.take_connection() {
|
||||||
|
let packet = peer.get_packet();
|
||||||
|
|
||||||
|
let (has_data, face_data) = LiveLinkFace::decode(packet.as_slice());
|
||||||
|
if has_data {
|
||||||
|
self.borrow_mut().face_data = Some(face_data);
|
||||||
|
let mut face_data_object = FaceDataObject::new_gd();
|
||||||
|
read_face_data(
|
||||||
|
&mut face_data_object,
|
||||||
|
&self.borrow_mut().face_data.take().unwrap(),
|
||||||
|
);
|
||||||
|
let fdo_variant = Variant::from(Some(face_data_object));
|
||||||
|
let changed = Variant::from(has_data);
|
||||||
|
self.base_mut()
|
||||||
|
.emit_signal("face_data_updated".into(), &[changed, fdo_variant]);
|
||||||
|
} else {
|
||||||
|
self.borrow_mut().face_data = None;
|
||||||
|
let changed = Variant::from(has_data);
|
||||||
|
let fdo_variant = Variant::from::<Option<Gd<FaceDataObject>>>(None);
|
||||||
|
self.base_mut()
|
||||||
|
.emit_signal("face_data_updated".into(), &[changed, fdo_variant]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn exit_tree(&mut self) {}
|
||||||
|
}
|
Loading…
Reference in a new issue