diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..8ad74f7 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Normalize EOL for all files that Git considers text files. +* text=auto eol=lf diff --git a/.github/workflows/build-on-push.yml b/.github/workflows/build-on-push.yml new file mode 100644 index 0000000..d4f465f --- /dev/null +++ b/.github/workflows/build-on-push.yml @@ -0,0 +1,59 @@ +# Workflow to automatically create deliverables +name: Build on push + +on: + [push, pull_request] + +jobs: + build: + name: Assembling artifacts + runs-on: ubuntu-20.04 + + # Note, to satisfy the asset library we need to make sure our zip files have a root folder + # this is why we checkout into demo/godot_rpm_avatar + # and build plugin/godot_rpm_avatar + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + path: demo/godot_rpm_avatar + - name: Create Godot RPM Avatar plugin + run: | + mkdir plugin + mkdir plugin/godot_rpm_avatar + mkdir plugin/godot_rpm_avatar/addons + cp -r demo/godot_rpm_avatar/addons/godot_rpm_avatar plugin/godot_rpm_avatar/addons + cp demo/godot_rpm_avatar/LICENSE plugin/godot_rpm_avatar/addons/godot_rpm_avatar + cp demo/godot_rpm_avatar/CONTRIBUTORS.md plugin/godot_rpm_avatar/addons/godot_rpm_avatar + cp demo/godot_rpm_avatar/VERSIONS.md plugin/godot_rpm_avatar/addons/godot_rpm_avatar + rm -rf demo/godot_rpm_avatar/.git + rm -rf demo/godot_rpm_avatar/.github + - name: Create Godot RPM Avatar library artifact + uses: actions/upload-artifact@v2 + with: + name: godot_rpm_avatar + path: | + plugin + - name: Create Godot RPM Avatar demo artifact + uses: actions/upload-artifact@v2 + with: + name: godot_rpm_avatar_demo + path: | + demo + - name: Zip asset + run: | + cd plugin + zip -qq -r ../godot_rpm_avatar.zip godot_rpm_avatar + cd ../demo + zip -qq -r ../godot_rpm_avatar_demo.zip godot_rpm_avatar + cd .. + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') + - name: Create and upload asset + uses: ncipollo/release-action@v1 + with: + allowUpdates: true + artifacts: "godot_rpm_avatar.zip,godot_rpm_avatar_demo.zip" + omitNameDuringUpdate: true + omitBodyDuringUpdate: true + token: ${{ secrets.GITHUB_TOKEN }} + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') diff --git a/.github/workflows/gdlint-on-push.yml b/.github/workflows/gdlint-on-push.yml new file mode 100644 index 0000000..572b938 --- /dev/null +++ b/.github/workflows/gdlint-on-push.yml @@ -0,0 +1,26 @@ +# Workflow to automatically lint gdscript code +name: gdlint on push + +on: + [push, pull_request] + +jobs: + gdlint: + name: gdlint scripts + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.10' + - name: Install Dependencies + run: | + python -m pip install --upgrade pip + python -m pip install 'gdtoolkit==4.*' + - name: Lint Godot VMC Tracker + run: | + gdlint addons/godot_rpm_avatar + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7de8ea5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +# Godot 4+ specific ignores +.godot/ +android/ diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md new file mode 100644 index 0000000..3ccb44b --- /dev/null +++ b/CONTRIBUTORS.md @@ -0,0 +1,11 @@ +Contributors +============ + +The main author of this project is [Malcolm Nixon](https://github.com/Malcolmnixon) who manages the source repository found at: +https://github.com/Malcolmnixon/GodotReadyPlayerMeAvatar + +Other people who have helped out by submitting fixes, enhancements, etc are: + +- TODO + +Want to be on this list? We would love your help. diff --git a/README.md b/README.md index 792df26..bec642d 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,32 @@ -# GodotReadyPlayerMeAvatar -Godot ReadyPlayerMe Avatar Reader +# Godot ReadyPlayerMe Avatar + +![GitHub forks](https://img.shields.io/github/forks/Malcolmnixon/GodotReadyPlayerMeAvatar?style=plastic) +![GitHub Repo stars](https://img.shields.io/github/stars/Malcolmnixon/GodotReadyPlayerMeAvatar?style=plastic) +![GitHub contributors](https://img.shields.io/github/contributors/Malcolmnixon/GodotReadyPlayerMeAvatar?style=plastic) +![GitHub](https://img.shields.io/github/license/Malcolmnixon/GodotReadyPlayerMeAvatar?style=plastic) + +This repository contains a ReadyPlayerMe avatar loader for Godot that can +load avatars at runtime from the internet or local files, and can configure +them to be driven through the XR tracker system. + +## Versions + +Official releases are tagged and can be found [here](https://github.com/Malcolmnixon/GodotXRAxisStudioTracker/releases). + +The following branches are in active development: +| Branch | Description | Godot version | +|-----------|-------------------------------|------------------| +| master | Current development branch | Godot 4.3-dev6+ | + +## Licensing + +Code in this repository is licensed under the MIT license. + +## About this repository + +This repository was created by Malcolm Nixon + +It is primarily maintained by: +- [Malcolm Nixon](https://github.com/Malcolmnixon/) + +For further contributors please see `CONTRIBUTORS.md` diff --git a/VERSIONS.md b/VERSIONS.md new file mode 100644 index 0000000..fbb6d86 --- /dev/null +++ b/VERSIONS.md @@ -0,0 +1,2 @@ +# 1.0.0 +- Initial Revision diff --git a/addons/godot_rpm_avatar/plugin.cfg b/addons/godot_rpm_avatar/plugin.cfg new file mode 100644 index 0000000..3a5f014 --- /dev/null +++ b/addons/godot_rpm_avatar/plugin.cfg @@ -0,0 +1,7 @@ +[plugin] + +name="Godot ReadyPlayerMe Avatar" +description="Godot ReadyPlayerMe Avatar plugin" +author="Malcolm Nixon and Contributors" +version="1.0.0" +script="plugin.gd" diff --git a/addons/godot_rpm_avatar/plugin.gd b/addons/godot_rpm_avatar/plugin.gd new file mode 100644 index 0000000..87c0754 --- /dev/null +++ b/addons/godot_rpm_avatar/plugin.gd @@ -0,0 +1,9 @@ +@tool +extends EditorPlugin + + +func _enter_tree(): + # Register our autoload downloader object + add_autoload_singleton( + "RpmLoader", + "res://addons/godot_rpm_avatar/rpm_loader.gd") diff --git a/addons/godot_rpm_avatar/rpm_body.gd b/addons/godot_rpm_avatar/rpm_body.gd new file mode 100644 index 0000000..167d8fd --- /dev/null +++ b/addons/godot_rpm_avatar/rpm_body.gd @@ -0,0 +1,182 @@ +class_name RpmBody + + +## ReadyPlayerMe Body Script +## +## This script converts ReadyPlayerMe avatars into Godot Humanoid format by +## renaming and rotating the bones. + + +# Mapping from RPM to Godot Humanoid bones +const _RPM_TO_HUMANOID = { + "Hips" : "Hips", + "Spine" : "Spine", + "Spine1" : "Chest", + "Spine2" : "UpperChest", + "Neck" : "Neck", + "Head" : "Head", + "LeftEye" : "LeftEye", + "RightEye" : "RightEye", + "LeftShoulder" : "LeftShoulder", + "LeftArm" : "LeftUpperArm", + "LeftForeArm" : "LeftLowerArm", + "LeftHand" : "LeftHand", + "LeftHandThumb1" : "LeftThumbMetacarpal", + "LeftHandThumb2" : "LeftThumbProximal", + "LeftHandThumb3" : "LeftThumbDistal", + "LeftHandIndex1" : "LeftIndexProximal", + "LeftHandIndex2" : "LeftIndexIntermediate", + "LeftHandIndex3" : "LeftIndexDistal", + "LeftHandMiddle1" : "LeftMiddleProximal", + "LeftHandMiddle2" : "LeftMiddleIntermediate", + "LeftHandMiddle3" : "LeftMiddleDistal", + "LeftHandRing1" : "LeftRingProximal", + "LeftHandRing2" : "LeftRingIntermediate", + "LeftHandRing3" : "LeftRingDistal", + "LeftHandPinky1" : "LeftLittleProximal", + "LeftHandPinky2" : "LeftLittleIntermediate", + "LeftHandPinky3" : "LeftLittleDistal", + "RightShoulder" : "RightShoulder", + "RightArm" : "RightUpperArm", + "RightForeArm" : "RightLowerArm", + "RightHand" : "RightHand", + "RightHandThumb1" : "RightThumbMetacarpal", + "RightHandThumb2" : "RightThumbProximal", + "RightHandThumb3" : "RightThumbDistal", + "RightHandIndex1" : "RightIndexProximal", + "RightHandIndex2" : "RightIndexIntermediate", + "RightHandIndex3" : "RightIndexDistal", + "RightHandMiddle1" : "RightMiddleProximal", + "RightHandMiddle2" : "RightMiddleIntermediate", + "RightHandMiddle3" : "RightMiddleDistal", + "RightHandRing1" : "RightRingProximal", + "RightHandRing2" : "RightRingIntermediate", + "RightHandRing3" : "RightRingDistal", + "RightHandPinky1" : "RightLittleProximal", + "RightHandPinky2" : "RightLittleIntermediate", + "RightHandPinky3" : "RightLittleDistal", + "LeftUpLeg" : "LeftUpperLeg", + "LeftLeg" : "LeftLowerLeg", + "LeftFoot" : "LeftFoot", + "LeftToeBase" : "LeftToes", + "RightUpLeg" : "RightUpperLeg", + "RightLeg" : "RightLowerLeg", + "RightFoot" : "RightFoot", + "RightToeBase" : "RightToes" +} + + +## Retarget a skeleton mesh to conform to the Godot Humanoid standard +static func retarget(src_skeleton : Skeleton3D) -> void: + # Rename the bones to Godot Humanoid + _rename_bones(src_skeleton) + + # Save the original skeleton global rest + var original_global_rest : Array[Transform3D] = [] + for i in src_skeleton.get_bone_count(): + original_global_rest.append(src_skeleton.get_bone_global_rest(i)) + + # Rotate the bones + _rotate_bones(src_skeleton) + + # Fix the skin to counteract the bone rotation + for mesh : MeshInstance3D in src_skeleton.find_children("*", "MeshInstance3D"): + var skin := mesh.skin + if not skin: continue + for i in skin.get_bind_count(): + var bone_name := skin.get_bind_name(i) + var bone_idx := src_skeleton.find_bone(bone_name) + if bone_idx < 0: continue + var adjust_transform := \ + src_skeleton.get_bone_global_rest(bone_idx).affine_inverse() * \ + original_global_rest[bone_idx] + skin.set_bind_pose(i, adjust_transform * skin.get_bind_pose(i)) + + # Move skeleton to rest + _to_rest(src_skeleton) + + +# This method renames the bones in the skeleton to the Godot Humanoid standard +static func _rename_bones(src_skeleton : Skeleton3D) -> void: + # Rename bones from RPM to Godot Humanoid + for i in src_skeleton.get_bone_count(): + var old_name := src_skeleton.get_bone_name(i) + var new_name := _RPM_TO_HUMANOID.get(old_name, "") + if new_name != "": + src_skeleton.set_bone_name(i, new_name) + + # Rename skin binds to match the new bone names + for mesh : MeshInstance3D in src_skeleton.find_children("*", "MeshInstance3D"): + var skin := mesh.skin + if not skin: continue + for i in skin.get_bind_count(): + var old_name := skin.get_bind_name(i) + var new_name := _RPM_TO_HUMANOID.get(old_name, "") + if new_name != "": + skin.set_bind_name(i, new_name) + + +# This method rotates the bones as defined in the Godot Humanoid standard +static func _rotate_bones(src_skeleton : Skeleton3D) -> void: + # Build the Godot Humanoid profile skeleton + var profile := SkeletonProfileHumanoid.new() + var prof_skeleton := Skeleton3D.new() + for i in profile.bone_size: # Create bones + prof_skeleton.add_bone(profile.get_bone_name(i)) + prof_skeleton.set_bone_rest(i, profile.get_reference_pose(i)) + for i in profile.bone_size: # Set bone parents + var parent := profile.find_bone(profile.get_bone_parent(i)) + if parent >= 0: + prof_skeleton.set_bone_parent(i, parent) + + # Save the diffs when rotating + var diffs : Array[Basis] = [] + diffs.resize(src_skeleton.get_bone_count()) + diffs.fill(Basis.IDENTITY) + + # Overwrite the axes + var bones_to_process := src_skeleton.get_parentless_bones() + while bones_to_process.size(): # Walk bones from root to leaf + var src_idx := bones_to_process[0] + bones_to_process.remove_at(0) + bones_to_process.append_array(src_skeleton.get_bone_children(src_idx)) + + # Get the parent global rest + var src_pg := Basis.IDENTITY + var src_parent_idx := src_skeleton.get_bone_parent(src_idx) + if src_parent_idx >= 0: + src_pg = src_skeleton.get_bone_global_rest(src_parent_idx).basis + + # Get the rotation as defined by the profile + var tgt_rot := Basis.IDENTITY + var prof_idx := profile.find_bone(src_skeleton.get_bone_name(src_idx)) + if prof_idx >= 0: + tgt_rot = src_pg.inverse() * prof_skeleton.get_bone_global_rest(prof_idx).basis + + # Save the differences for each bone + if src_parent_idx >= 0: + diffs[src_idx] = \ + tgt_rot.inverse() * \ + diffs[src_parent_idx] * \ + src_skeleton.get_bone_rest(src_idx).basis + else: + diffs[src_idx] = tgt_rot.inverse() * src_skeleton.get_bone_rest(src_idx).basis + + var diff := Basis.IDENTITY + if src_parent_idx >= 0: + diff = diffs[src_parent_idx] + + src_skeleton.set_bone_rest( + src_idx, + Transform3D( + tgt_rot, + diff * src_skeleton.get_bone_rest(src_idx).origin)) + + +# This method moves the skeleton pose to the rest pose +static func _to_rest(src_skeleton : Skeleton3D) -> void: + # Init skeleton pose to new rest + for i in src_skeleton.get_bone_count(): + var fixed_rest := src_skeleton.get_bone_rest(i) + src_skeleton.set_bone_pose_position(i, fixed_rest.origin) + src_skeleton.set_bone_pose_rotation(i, fixed_rest.basis.get_rotation_quaternion()) diff --git a/addons/godot_rpm_avatar/rpm_loader.gd b/addons/godot_rpm_avatar/rpm_loader.gd new file mode 100644 index 0000000..33c36d1 --- /dev/null +++ b/addons/godot_rpm_avatar/rpm_loader.gd @@ -0,0 +1,228 @@ +extends Node + + +## ReadyPlayerMe Loader Node +## +## This node loads ReadyPlayerMe avatars from either the web or files. The +## avatars are processed in worker threads to prevent stalling the renderer. + + +## Signal invoked when an avatar finishes loading +signal load_complete(id : String, avatar : Node3D) + +## Signal invoked when an avatar load fails +signal load_failed(id : String, reason : String) + + +# Import flags to generate tangent arrays and use named skin bindings +const _GLTF_FLAGS : int = 0x18 + +# ReadyPlayerMe URL +const _RPM_URL := \ + "https://models.readyplayer.me/{id}.glb" + \ + "?quality={quality}" + \ + "&pose=T" + \ + "&morphTargets={morph}" + +# ReadyPlayerMe quality strings +const _QUALITY : Array[String] = [ + "low", + "medium", + "high" +] + + +# ReadyPlayerMe Download Request +class RpmDownloadRequest: + var id : String + var url : String + var settings : RpmSettings + + func _init( + _id : String, + _url : String, + _settings : RpmSettings) -> void: + # Save the parameters + id = _id + url = _url + settings = _settings + + +# HTTP Request instance +var _http_request : HTTPRequest + +# Queue of download requests +var _queue : Array[RpmDownloadRequest] = [] + +# Current download request +var _current : RpmDownloadRequest + + +## Called when the node is ready +func _ready() -> void: + # Construct the HTTP Request + _http_request = HTTPRequest.new() + add_child(_http_request) + + # Subscribe to the request completed event and start the first download. + _http_request.request_completed.connect(_on_http_request_completed) + _download_next() + + +## Queue loading an avatar from the web. +func load_web( + id : String, + settings : RpmSettings) -> void: + # Construct the ReadyPlayerMe download URL + var url := _RPM_URL.format({ + "id": id, + "quality": _QUALITY[settings.quality], + "morph": "ARKit" if settings.face_tracker else "Default" + }) + + # Construct the request + print_verbose("RpmLoader: load_web - id=", id, " url=", url) + _queue.push_back(RpmDownloadRequest.new(id, url, settings)) + _download_next() + + +## Queue loading an avatar from file. +func load_file( + id : String, + file_name : String, + settings : RpmSettings) -> void: + # Load in a worker thread + print_verbose("RpmLoader: load_file - id=", id, " file=", file_name) + WorkerThreadPool.add_task( + _threaded_load_file.bind( + id, + file_name, + settings)) + + +# Start the next download if possible +func _download_next() -> void: + # Skip if busy or not started + if _current or not _http_request: + return + + # Start the next download + _current = _queue.pop_front() + if _current: + print_verbose("RpmLoader: downloading - id=", _current.id, " url=", _current.url) + _http_request.request(_current.url) + + +# Handle downloading of the avatar +func _on_http_request_completed( + result : int, + _response_code : int, + _headers : PackedStringArray, + body : PackedByteArray) -> void: + # Handle completion + if result == HTTPRequest.RESULT_SUCCESS: + # Load in a worker thread + print_verbose("RpmLoader: loading - id=", _current.id) + WorkerThreadPool.add_task( + _threaded_load_buffer.bind( + _current.id, + body, + _current.settings)) + else: + # Report the download failure + print_verbose("RpmLoader: download-failed - id=", _current.id, " result=", result) + _load_failed.call_deferred(_current.id, "Download Failed") + + # Start the next download + _current = null + _download_next() + + +# Load the avatar file +func _threaded_load_file( + id : String, + file_name : String, + settings : RpmSettings) -> void: + # Load the GLTF document from file + var doc := GLTFDocument.new() + var state := GLTFState.new() + state.set_handle_binary_image(GLTFState.HANDLE_BINARY_EMBED_AS_BASISU) + doc.append_from_file(file_name, state, _GLTF_FLAGS) + + # Load the avatar + _load_gltf(id, doc, state, settings) + + +# Load the avatar buffer +func _threaded_load_buffer( + id : String, + buffer : PackedByteArray, + settings : RpmSettings) -> void: + # Load the GLTF document from buffer + var doc := GLTFDocument.new() + var state := GLTFState.new() + state.set_handle_binary_image(GLTFState.HANDLE_BINARY_EMBED_AS_BASISU) + doc.append_from_buffer(buffer, "", state, _GLTF_FLAGS) + + # Load the avatar + _load_gltf(id, doc, state, settings) + + +# Load the avatar +func _load_gltf( + id : String, + doc : GLTFDocument, + state : GLTFState, + settings : RpmSettings) -> void: + # Generate the scene + print_verbose("RpmLoader: generating - id=", id) + var scene := doc.generate_scene(state) + + # Find the skeleton + var skeletons := scene.find_children("*", "Skeleton3D") + if skeletons.size() != 1: + _load_failed.call_deferred(id, "Corrupt Avatar") + return + + # Find the mesh + var meshes := scene.find_children("*", "MeshInstance3D") + if meshes.size() != 1: + _load_failed.call_deferred(id, "Corrupt Avatar") + return + + # Construct the nodes + var skeleton := skeletons[0] as Skeleton3D + var mesh := meshes[0] as MeshInstance3D + + # Retarget the skeleton to Godot Humanoid + RpmBody.retarget(skeleton) + + # Construct the XRBodyModifier3D + var body_modifier := XRBodyModifier3D.new() + body_modifier.add_child(scene) + + # Configure the XRBodyModifier3D + body_modifier.body_tracker = settings.body_tracker + body_modifier.target = body_modifier.get_path_to(skeleton) + body_modifier.bone_update = XRBodyModifier3D.BONE_UPDATE_ROTATION_ONLY + + # Construct and append the XRFaceModifier3D + if settings.face_tracker != "": + var face_modifier := XRFaceModifier3D.new() + body_modifier.add_child(face_modifier) + face_modifier.face_tracker = settings.face_tracker + face_modifier.target = face_modifier.get_path_to(mesh) + + # Report the load completed + print_verbose("RpmLoader: loaded - id=", id) + _load_complete.call_deferred(id, body_modifier) + + +# Report load complete +func _load_complete(id : String, avatar : Node3D) -> void: + load_complete.emit(id, avatar) + + +# Report load failed +func _load_failed(id : String) -> void: + load_failed.emit(id) diff --git a/addons/godot_rpm_avatar/rpm_settings.gd b/addons/godot_rpm_avatar/rpm_settings.gd new file mode 100644 index 0000000..41eb476 --- /dev/null +++ b/addons/godot_rpm_avatar/rpm_settings.gd @@ -0,0 +1,25 @@ +class_name RpmSettings +extends Resource + + +## ReadyPlayerMe Settings Resource +## +## This resource defines the settings for reading ReadyPlayerMe avatars. + + +## Avatar Quality Options +enum Quality { + QUALITY_LOW, ## Low Quality Avatar + QUALITY_MEDIUM, ## Medium Quality Avatar + QUALITY_HIGH ## High Quality Avatar +} + + +## Body tracker name +@export var body_tracker : String = "" + +## Face tracker name +@export var face_tracker : String = "" + +## Avatar quality +@export var quality : Quality = Quality.QUALITY_MEDIUM diff --git a/addons/godot_vmc_tracker/CONTRIBUTORS.md b/addons/godot_vmc_tracker/CONTRIBUTORS.md new file mode 100644 index 0000000..9271691 --- /dev/null +++ b/addons/godot_vmc_tracker/CONTRIBUTORS.md @@ -0,0 +1,11 @@ +Contributors +============ + +The main author of this project is [Malcolm Nixon](https://github.com/Malcolmnixon) who manages the source repository found at: +https://github.com/Malcolmnixon/GodotXRVmcTracker + +Other people who have helped out by submitting fixes, enhancements, etc are: + +- K. S. Ernest (iFire) Lee ([fire](https://github.com/fire)) + +Want to be on this list? We would love your help. diff --git a/addons/godot_vmc_tracker/LICENSE b/addons/godot_vmc_tracker/LICENSE new file mode 100644 index 0000000..6b35e34 --- /dev/null +++ b/addons/godot_vmc_tracker/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Malcolm Nixon + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/addons/godot_vmc_tracker/VERSIONS.md b/addons/godot_vmc_tracker/VERSIONS.md new file mode 100644 index 0000000..0d3e406 --- /dev/null +++ b/addons/godot_vmc_tracker/VERSIONS.md @@ -0,0 +1,8 @@ +# 1.1.0 +- Set tracker "Root" under hips +- Move avatars under body modifier +- Minor code cleanup +- Added ability to instantiate multiple tracker sources + +# 1.0.0 +- Initial Revision diff --git a/addons/godot_vmc_tracker/osc_reader.gd b/addons/godot_vmc_tracker/osc_reader.gd new file mode 100644 index 0000000..d8b6299 --- /dev/null +++ b/addons/godot_vmc_tracker/osc_reader.gd @@ -0,0 +1,140 @@ +class_name OSCReader +extends Object + + +## OSC Reader Script +## +## This script implements a basic OSC packet reader. The listen method is used +## to start the UDP server. The poll method should be called to poll for +## incoming packets. Packets are decoded and dispatched through the +## on_osc_packet signal. + + +## OSC packet received signal +signal on_osc_packet(data : Array) + + +# UDP Server +var _server : UDPServer = UDPServer.new() + +# Current connection +var _connection : PacketPeerUDP + + +## Stop listening +func stop() -> void: + _server.stop() + _connection = null + + +## Start listening +func listen(p_port : int = 39539) -> void: + stop() + _server.listen(p_port) + + +## Poll for incoming packets +func poll() -> void: + # Poll the server + _server.poll() + + # Switch to any new connection + if _server.is_connection_available(): + _connection = _server.take_connection() + + # Skip if no connection + if not _connection: + return + + # Loop processing the incoming packets + while _connection.get_available_packet_count() > 0: + # Read the packet + var packet := StreamPeerBuffer.new() + packet.big_endian = true + packet.data_array = _connection.get_packet() + + # Read the packet + var data := [] + _read_osc_message_bundle(packet, data) + + # Dispatch the data + on_osc_packet.emit(data) + + +# Read an OSC message or bundle +func _read_osc_message_bundle(packet : StreamPeerBuffer, data : Array) -> void: + # Inspect the data item + var type := packet.data_array[packet.get_position()] + match type: + 47: # '/' character starting OSC message + _read_osc_message(packet, data) + + 35: # '#' character starting OSC bundle + _read_osc_bundle(packet, data) + + +# Read an OSC message +func _read_osc_message(packet : StreamPeerBuffer, data : Array) -> void: + var values := [] + values.append(_read_osc_string(packet)) + var type := _read_osc_string(packet) + for ch in type: + match ch: + "i": + values.append(packet.get_32()) + "f": + values.append(packet.get_float()) + "s": + values.append(_read_osc_string(packet)) + "b": + values.append(_read_osc_blob(packet)) + + data.append(values) + + +# Read an OSC bundle +func _read_osc_bundle(packet : StreamPeerBuffer, data : Array) -> void: + var bundle := _read_osc_string(packet) + var time := packet.get_64() + + while packet.get_available_bytes() > 4: + _read_osc_bundle_element(packet, data) + + +# Read an OSC bundle element +func _read_osc_bundle_element(packet : StreamPeerBuffer, data : Array) -> void: + var size := packet.get_32() + _read_osc_message_bundle(packet, data) + + +# Read an OSC string +func _read_osc_string(packet : StreamPeerBuffer) -> String: + # Find the end of the string + var pos := packet.get_position() + var end := packet.data_array.find(0, pos) + if end < 0: + return ""; + + # Read the string + var value := packet.get_string(end - pos) + + # Seek past the null(s) + packet.seek((end + 4) & ~3) + + # Return the string + return value + + +# Read an OSC blob +static func _read_osc_blob(packet : StreamPeerBuffer) -> PackedByteArray: + # Read the size + var size := packet.get_32() + + # Read the data + var data := packet.get_data(size) + + # Seek past the null padding + packet.seek((packet.get_position() + 3) & ~3) + + # Return the blob + return data[1] diff --git a/addons/godot_vmc_tracker/plugin.cfg b/addons/godot_vmc_tracker/plugin.cfg new file mode 100644 index 0000000..60937b6 --- /dev/null +++ b/addons/godot_vmc_tracker/plugin.cfg @@ -0,0 +1,7 @@ +[plugin] + +name="Godot VMC Tracker" +description="Godot VMC Tracker plugin" +author="Malcolm Nixon and Contributors" +version="1.0.0" +script="plugin.gd" diff --git a/addons/godot_vmc_tracker/plugin.gd b/addons/godot_vmc_tracker/plugin.gd new file mode 100644 index 0000000..dbcd924 --- /dev/null +++ b/addons/godot_vmc_tracker/plugin.gd @@ -0,0 +1,71 @@ +@tool +extends EditorPlugin + + +func _define_project_setting( + p_name : String, + p_type : int, + p_hint : int = PROPERTY_HINT_NONE, + p_hint_string : String = "", + p_default_val = "") -> void: + # p_default_val can be any type!! + + if !ProjectSettings.has_setting(p_name): + ProjectSettings.set_setting(p_name, p_default_val) + + var property_info : Dictionary = { + "name" : p_name, + "type" : p_type, + "hint" : p_hint, + "hint_string" : p_hint_string + } + + ProjectSettings.add_property_info(property_info) + if ProjectSettings.has_method("set_as_basic"): + ProjectSettings.call("set_as_basic", p_name, true) + ProjectSettings.set_initial_value(p_name, p_default_val) + + + +func _enter_tree(): + # Add face tracker name + _define_project_setting( + "godot_vmc_tracker/tracking/face_tracker_name", + TYPE_STRING, + PROPERTY_HINT_NONE, + "", + "/vmc/head") + + # Add body tracker name + _define_project_setting( + "godot_vmc_tracker/tracking/body_tracker_name", + TYPE_STRING, + PROPERTY_HINT_NONE, + "", + "/vmc/body") + + # Add position mode + _define_project_setting( + "godot_vmc_tracker/tracking/position_mode", + TYPE_INT, + PROPERTY_HINT_ENUM, + "Free,Calibrate,Locked", + 0) + + # Add network port + _define_project_setting( + "godot_vmc_tracker/network/udp_listener_port", + TYPE_INT, + PROPERTY_HINT_NONE, + "", + 39539) + + # Register our autoload user settings object + add_autoload_singleton( + "VmcPlugin", + "res://addons/godot_vmc_tracker/vmc_plugin.gd") + + +func _exit_tree(): + # our plugin is turned off + pass diff --git a/addons/godot_vmc_tracker/vmc_body.gd b/addons/godot_vmc_tracker/vmc_body.gd new file mode 100644 index 0000000..015fb65 --- /dev/null +++ b/addons/godot_vmc_tracker/vmc_body.gd @@ -0,0 +1,813 @@ +class_name VMCBody + + +## Constants for VMC Body +## +## This script contains the definition required to interpret the VMC body +## and translate it to XRBodyTracker and XRFaceTracker format. + + +## Enumeration of VMC joints +enum Joint { + HIPS = 0, + SPINE = 1, + CHEST = 2, + UPPER_CHEST = 3, + NECK = 4, + HEAD = 5, + LEFT_EYE = 6, + RIGHT_EYE = 7, + JAW = 8, + LEFT_UPPER_LEG = 9, + LEFT_LOWER_LEG = 10, + LEFT_FOOT = 11, + LEFT_TOES = 12, + RIGHT_UPPER_LEG = 13, + RIGHT_LOWER_LEG = 14, + RIGHT_FOOT = 15, + RIGHT_TOES = 16, + LEFT_SHOULDER = 17, + LEFT_UPPER_ARM = 18, + LEFT_LOWER_ARM = 19, + LEFT_HAND = 20, + RIGHT_SHOULDER = 21, + RIGHT_UPPER_ARM = 22, + RIGHT_LOWER_ARM = 23, + RIGHT_HAND = 24, + LEFT_THUMB_PROXIMAL = 25, + LEFT_THUMB_INTERMEDIATE = 26, + LEFT_THUMB_DISTAL = 27, + LEFT_INDEX_PROXIMAL = 28, + LEFT_INDEX_INTERMEDIATE = 29, + LEFT_INDEX_DISTAL = 30, + LEFT_MIDDLE_PROXIMAL = 31, + LEFT_MIDDLE_INTERMEDIATE = 32, + LEFT_MIDDLE_DISTAL = 33, + LEFT_RING_PROXIMAL = 34, + LEFT_RING_INTERMEDIATE = 35, + LEFT_RING_DISTAL = 36, + LEFT_LITTLE_PROXIMAL = 37, + LEFT_LITTLE_INTERMEDIATE = 38, + LEFT_LITTLE_DISTAL = 39, + RIGHT_THUMB_PROXIMAL = 40, + RIGHT_THUMB_INTERMEDIATE = 41, + RIGHT_THUMB_DISTAL = 42, + RIGHT_INDEX_PROXIMAL = 43, + RIGHT_INDEX_INTERMEDIATE = 44, + RIGHT_INDEX_DISTAL = 45, + RIGHT_MIDDLE_PROXIMAL = 46, + RIGHT_MIDDLE_INTERMEDIATE = 47, + RIGHT_MIDDLE_DISTAL = 48, + RIGHT_RING_PROXIMAL = 49, + RIGHT_RING_INTERMEDIATE = 50, + RIGHT_RING_DISTAL = 51, + RIGHT_LITTLE_PROXIMAL = 52, + RIGHT_LITTLE_INTERMEDIATE = 53, + RIGHT_LITTLE_DISTAL = 54, + COUNT = 55 +} + +## Enumeration of VMC face blends +enum FaceBlend { + EYE_LOOK_UP_LEFT = 0, + EYE_LOOK_UP_RIGHT = 1, + EYE_LOOK_DOWN_LEFT = 2, + EYE_LOOK_DOWN_RIGHT = 3, + EYE_LOOK_IN_LEFT = 4, + EYE_LOOK_IN_RIGHT = 5, + EYE_LOOK_OUT_LEFT = 6, + EYE_LOOK_OUT_RIGHT = 7, + EYE_BLINK_LEFT = 8, + EYE_BLINK_RIGHT = 9, + EYE_SQUINT_LEFT = 10, + EYE_SQUINT_RIGHT = 11, + EYE_WIDE_LEFT = 12, + EYE_WIDE_RIGHT = 13, + BROW_DOWN_LEFT = 14, + BROW_DOWN_RIGHT = 15, + BROW_INNER_UP = 16, + BROW_OUTER_UP_LEFT = 17, + BROW_OUTER_UP_RIGHT = 18, + NOSE_SNEER_LEFT = 19, + NOSE_SNEER_RIGHT = 20, + CHEEK_SQUINT_LEFT = 21, + CHEEK_SQUINT_RIGHT = 22, + CHEEK_PUFF = 23, + JAW_OPEN = 24, + MOUTH_CLOSE = 25, + JAW_RIGHT = 26, + JAW_LEFT = 27, + JAW_FORWARD = 28, + MOUTH_ROLL_UPPER = 29, + MOUTH_ROLL_LOWER = 30, + MOUTH_FUNNEL = 31, + MOUTH_PUCKER = 32, + MOUTH_UPPER_UP_LEFT = 33, + MOUTH_UPPER_UP_RIGHT = 34, + MOUTH_LOWER_DOWN_LEFT = 35, + MOUTH_LOWER_DOWN_RIGHT = 36, + MOUTH_SMILE_LEFT = 37, + MOUTH_SMILE_RIGHT = 38, + MOUTH_FROWN_LEFT = 39, + MOUTH_FROWN_RIGHT = 40, + MOUTH_STRETCH_LEFT = 41, + MOUTH_STRETCH_RIGHT = 42, + MOUTH_DIMPLE_LEFT = 43, + MOUTH_DIMPLE_RIGHT = 44, + MOUTH_SHRUG_UPPER = 45, + MOUTH_SHRUG_LOWER = 46, + MOUTH_PRESS_LEFT = 47, + MOUTH_PRESS_RIGHT = 48, + TONGUE_OUT = 49, + COUNT = 50 +} + +## Dictionary of VMC Joint names to joints +const JOINT_NAMES := { + &"Hips" : Joint.HIPS, + &"Spine" : Joint.SPINE, + &"Chest" : Joint.CHEST, + &"UpperChest" : Joint.UPPER_CHEST, + &"Neck" : Joint.NECK, + &"Head" : Joint.HEAD, + &"LeftEye" : Joint.LEFT_EYE, + &"RightEye" : Joint.RIGHT_EYE, + &"Jaw" : Joint.JAW, + &"LeftUpperLeg" : Joint.LEFT_UPPER_LEG, + &"LeftLowerLeg" : Joint.LEFT_LOWER_LEG, + &"LeftFoot" : Joint.LEFT_FOOT, + &"LeftToes" : Joint.LEFT_TOES, + &"RightUpperLeg" : Joint.RIGHT_UPPER_LEG, + &"RightLowerLeg" : Joint.RIGHT_LOWER_LEG, + &"RightFoot" : Joint.RIGHT_FOOT, + &"RightToes" : Joint.RIGHT_TOES, + &"LeftShoulder" : Joint.LEFT_SHOULDER, + &"LeftUpperArm" : Joint.LEFT_UPPER_ARM, + &"LeftLowerArm" : Joint.LEFT_LOWER_ARM, + &"LeftHand" : Joint.LEFT_HAND, + &"RightShoulder" : Joint.RIGHT_SHOULDER, + &"RightUpperArm" : Joint.RIGHT_UPPER_ARM, + &"RightLowerArm" : Joint.RIGHT_LOWER_ARM, + &"RightHand" : Joint.RIGHT_HAND, + &"LeftThumbProximal" : Joint.LEFT_THUMB_PROXIMAL, + &"LeftThumbIntermediate" : Joint.LEFT_THUMB_INTERMEDIATE, + &"LeftThumbDistal" : Joint.LEFT_THUMB_DISTAL, + &"LeftIndexProximal" : Joint.LEFT_INDEX_PROXIMAL, + &"LeftIndexIntermediate" : Joint.LEFT_INDEX_INTERMEDIATE, + &"LeftIndexDistal" : Joint.LEFT_INDEX_DISTAL, + &"LeftMiddleProximal" : Joint.LEFT_MIDDLE_PROXIMAL, + &"LeftMiddleIntermediate" : Joint.LEFT_MIDDLE_INTERMEDIATE, + &"LeftMiddleDistal" : Joint.LEFT_MIDDLE_DISTAL, + &"LeftRingProximal" : Joint.LEFT_RING_PROXIMAL, + &"LeftRingIntermediate" : Joint.LEFT_RING_INTERMEDIATE, + &"LeftRingDistal" : Joint.LEFT_RING_DISTAL, + &"LeftLittleProximal" : Joint.LEFT_LITTLE_PROXIMAL, + &"LeftLittleIntermediate" : Joint.LEFT_LITTLE_INTERMEDIATE, + &"LeftLittleDistal" : Joint.LEFT_LITTLE_DISTAL, + &"RightThumbProximal" : Joint.RIGHT_THUMB_PROXIMAL, + &"RightThumbIntermediate" : Joint.RIGHT_THUMB_INTERMEDIATE, + &"RightThumbDistal" : Joint.RIGHT_THUMB_DISTAL, + &"RightIndexProximal" : Joint.RIGHT_INDEX_PROXIMAL, + &"RightIndexIntermediate" : Joint.RIGHT_INDEX_INTERMEDIATE, + &"RightIndexDistal" : Joint.RIGHT_INDEX_DISTAL, + &"RightMiddleProximal" : Joint.RIGHT_MIDDLE_PROXIMAL, + &"RightMiddleIntermediate" : Joint.RIGHT_MIDDLE_INTERMEDIATE, + &"RightMiddleDistal" : Joint.RIGHT_MIDDLE_DISTAL, + &"RightRingProximal" : Joint.RIGHT_RING_PROXIMAL, + &"RightRingIntermediate" : Joint.RIGHT_RING_INTERMEDIATE, + &"RightRingDistal" : Joint.RIGHT_RING_DISTAL, + &"RightLittleProximal" : Joint.RIGHT_LITTLE_PROXIMAL, + &"RightLittleIntermediate" : Joint.RIGHT_LITTLE_INTERMEDIATE, + &"RightLittleDistal" : Joint.RIGHT_LITTLE_DISTAL +} + +## Dictionary of VMC Face Blend names to face blends +const FACE_BLEND_NAMES := { + &"EyeLookUpLeft" : FaceBlend.EYE_LOOK_UP_LEFT, + &"EyeLookUpRight" : FaceBlend.EYE_LOOK_UP_RIGHT, + &"EyeLookDownLeft" : FaceBlend.EYE_LOOK_DOWN_LEFT, + &"EyeLookDownRight" : FaceBlend.EYE_LOOK_DOWN_RIGHT, + &"EyeLookInLeft" : FaceBlend.EYE_LOOK_IN_LEFT, + &"EyeLookInRight" : FaceBlend.EYE_LOOK_IN_RIGHT, + &"EyeLookOutLeft" : FaceBlend.EYE_LOOK_OUT_LEFT, + &"EyeLookOutRight" : FaceBlend.EYE_LOOK_OUT_RIGHT, + &"EyeBlinkLeft" : FaceBlend.EYE_BLINK_LEFT, + &"EyeBlinkRight" : FaceBlend.EYE_BLINK_RIGHT, + &"EyeSquintLeft" : FaceBlend.EYE_SQUINT_LEFT, + &"EyeSquintRight" : FaceBlend.EYE_SQUINT_RIGHT, + &"EyeWideLeft" : FaceBlend.EYE_WIDE_LEFT, + &"EyeWideRight" : FaceBlend.EYE_WIDE_RIGHT, + &"BrowDownLeft" : FaceBlend.BROW_DOWN_LEFT, + &"BrowDownRight" : FaceBlend.BROW_DOWN_RIGHT, + &"BrowInnerUp" : FaceBlend.BROW_INNER_UP, + &"BrowOuterUpLeft" : FaceBlend.BROW_OUTER_UP_LEFT, + &"BrowOuterUpRight" : FaceBlend.BROW_OUTER_UP_RIGHT, + &"NoseSneerLeft" : FaceBlend.NOSE_SNEER_LEFT, + &"NoseSneerRight" : FaceBlend.NOSE_SNEER_RIGHT, + &"CheekSquintLeft" : FaceBlend.CHEEK_SQUINT_LEFT, + &"CheekSquintRight" : FaceBlend.CHEEK_SQUINT_RIGHT, + &"CheekPuff" : FaceBlend.CHEEK_PUFF, + &"JawOpen" : FaceBlend.JAW_OPEN, + &"MouthClose" : FaceBlend.MOUTH_CLOSE, + &"JawRight" : FaceBlend.JAW_RIGHT, + &"JawLeft" : FaceBlend.JAW_LEFT, + &"JawForward" : FaceBlend.JAW_FORWARD, + &"MouthRollUpper" : FaceBlend.MOUTH_ROLL_UPPER, + &"MouthRollLower" : FaceBlend.MOUTH_ROLL_LOWER, + &"MouthFunnel" : FaceBlend.MOUTH_FUNNEL, + &"MouthPucker" : FaceBlend.MOUTH_PUCKER, + &"MouthUpperUpLeft" : FaceBlend.MOUTH_UPPER_UP_LEFT, + &"MouthUpperUpRight" : FaceBlend.MOUTH_UPPER_UP_RIGHT, + &"MouthLowerDownLeft" : FaceBlend.MOUTH_LOWER_DOWN_LEFT, + &"MouthLowerDownRight" : FaceBlend.MOUTH_LOWER_DOWN_RIGHT, + &"MouthSmileLeft" : FaceBlend.MOUTH_SMILE_LEFT, + &"MouthSmileRight" : FaceBlend.MOUTH_SMILE_RIGHT, + &"MouthFrownLeft" : FaceBlend.MOUTH_FROWN_LEFT, + &"MouthFrownRight" : FaceBlend.MOUTH_FROWN_RIGHT, + &"MouthStretchLeft" : FaceBlend.MOUTH_STRETCH_LEFT, + &"MouthStretchRight" : FaceBlend.MOUTH_STRETCH_RIGHT, + &"MouthDimpleLeft" : FaceBlend.MOUTH_DIMPLE_LEFT, + &"MouthDimpleRight" : FaceBlend.MOUTH_DIMPLE_RIGHT, + &"MouthShrugUpper" : FaceBlend.MOUTH_SHRUG_UPPER, + &"MouthShrugLower" : FaceBlend.MOUTH_SHRUG_LOWER, + &"MouthPressLeft" : FaceBlend.MOUTH_PRESS_LEFT, + &"MouthPressRight" : FaceBlend.MOUTH_PRESS_RIGHT, + &"TongueOut" : FaceBlend.TONGUE_OUT, +} + +## VMC Joint Parent relationship +const JOINT_PARENT : Array[Joint] = [ + -1, # 0: Joint.HIPS + Joint.HIPS, # 1: Joint.SPINE + Joint.SPINE, # 2: Joint.CHEST + Joint.CHEST, # 3: Joint.UPPER_CHEST + Joint.UPPER_CHEST, # 4: Joint.NECK + Joint.NECK, # 5: Joint.HEAD + Joint.HEAD, # 6: Joint.LEFT_EYE + Joint.HEAD, # 7: Joint.RIGHT_EYE + Joint.HEAD, # 8: Joint.JAW + Joint.HIPS, # 9: Joint.LEFT_UPPER_LEG + Joint.LEFT_UPPER_LEG, # 10: Joint.LEFT_LOWER_LEG + Joint.LEFT_LOWER_LEG, # 11: Joint.LEFT_FOOT + Joint.LEFT_FOOT, # 12: Joint.LEFT_TOES + Joint.HIPS, # 13: Joint.RIGHT_UPPER_LEG + Joint.RIGHT_UPPER_LEG, # 14: Joint.RIGHT_LOWER_LEG + Joint.RIGHT_LOWER_LEG, # 15: Joint.RIGHT_FOOT + Joint.RIGHT_FOOT, # 16: Joint.RIGHT_TOES + Joint.UPPER_CHEST, # 17: Joint.LEFT_SHOULDER + Joint.LEFT_SHOULDER, # 18: Joint.LEFT_UPPER_ARM + Joint.LEFT_UPPER_ARM, # 19: Joint.LEFT_LOWER_ARM + Joint.LEFT_LOWER_ARM, # 20: Joint.LEFT_HAND + Joint.UPPER_CHEST, # 21: Joint.RIGHT_SHOULDER + Joint.RIGHT_SHOULDER, # 22: Joint.RIGHT_UPPER_ARM + Joint.RIGHT_UPPER_ARM, # 23: Joint.RIGHT_LOWER_ARM + Joint.RIGHT_LOWER_ARM, # 24: Joint.RIGHT_HAND + Joint.LEFT_HAND, # 25: Joint.LEFT_THUMB_PROXIMAL + Joint.LEFT_THUMB_PROXIMAL, # 26: Joint.LEFT_THUMB_INTERMEDIATE + Joint.LEFT_THUMB_INTERMEDIATE, # 27: Joint.LEFT_THUMB_DISTAL + Joint.LEFT_HAND, # 28: Joint.LEFT_INDEX_PROXIMAL + Joint.LEFT_INDEX_PROXIMAL, # 29: Joint.LEFT_INDEX_INTERMEDIATE + Joint.LEFT_INDEX_INTERMEDIATE, # 30: Joint.LEFT_INDEX_DISTAL + Joint.LEFT_HAND, # 31: Joint.LEFT_MIDDLE_PROXIMAL + Joint.LEFT_MIDDLE_PROXIMAL, # 32: Joint.LEFT_MIDDLE_INTERMEDIATE + Joint.LEFT_MIDDLE_INTERMEDIATE, # 33: Joint.LEFT_MIDDLE_DISTAL + Joint.LEFT_HAND, # 34: Joint.LEFT_RING_PROXIMAL + Joint.LEFT_RING_PROXIMAL, # 35: Joint.LEFT_RING_INTERMEDIATE + Joint.LEFT_RING_INTERMEDIATE, # 36: Joint.LEFT_RING_DISTAL + Joint.LEFT_HAND, # 37: Joint.LEFT_LITTLE_PROXIMAL + Joint.LEFT_LITTLE_PROXIMAL, # 38: Joint.LEFT_LITTLE_INTERMEDIATE + Joint.LEFT_LITTLE_INTERMEDIATE, # 39: Joint.LEFT_LITTLE_DISTAL + Joint.RIGHT_HAND, # 40: Joint.RIGHT_THUMB_PROXIMAL + Joint.RIGHT_THUMB_PROXIMAL, # 41: Joint.RIGHT_THUMB_INTERMEDIATE + Joint.RIGHT_THUMB_INTERMEDIATE, # 42: Joint.RIGHT_THUMB_DISTAL + Joint.RIGHT_HAND, # 43: Joint.RIGHT_INDEX_PROXIMAL + Joint.RIGHT_INDEX_PROXIMAL, # 44: Joint.RIGHT_INDEX_INTERMEDIATE + Joint.RIGHT_INDEX_INTERMEDIATE, # 45: Joint.RIGHT_INDEX_DISTAL + Joint.RIGHT_HAND, # 46: Joint.RIGHT_MIDDLE_PROXIMAL + Joint.RIGHT_MIDDLE_PROXIMAL, # 47: Joint.RIGHT_MIDDLE_INTERMEDIATE + Joint.RIGHT_MIDDLE_INTERMEDIATE,# 48: Joint.RIGHT_MIDDLE_DISTAL + Joint.RIGHT_HAND, # 49: Joint.RIGHT_RING_PROXIMAL + Joint.RIGHT_RING_PROXIMAL, # 50: Joint.RIGHT_RING_INTERMEDIATE + Joint.RIGHT_RING_INTERMEDIATE, # 51: Joint.RIGHT_RING_DISTAL + Joint.RIGHT_HAND, # 52: Joint.RIGHT_LITTLE_PROXIMAL + Joint.RIGHT_LITTLE_PROXIMAL, # 53: Joint.RIGHT_LITTLE_INTERMEDIATE + Joint.RIGHT_LITTLE_INTERMEDIATE,# 54: Joint.RIGHT_LITTLE_DISTAL +] + +## Mapping of XRBodyTracker joints to VMC joints +const JOINT_MAPPING : Array[Dictionary] = [ + # Upper Body Joints + { + body = XRBodyTracker.JOINT_HIPS, + native = Joint.HIPS, + roll = Quaternion(0.0, 1.0, 0.0, 0.0) + }, + { + body = XRBodyTracker.JOINT_SPINE, + native = Joint.SPINE, + roll = Quaternion(0.0, 1.0, 0.0, 0.0) + }, + { + body = XRBodyTracker.JOINT_CHEST, + native = Joint.CHEST, + roll = Quaternion(0.0, 1.0, 0.0, 0.0) + }, + { + body = XRBodyTracker.JOINT_UPPER_CHEST, + native = Joint.UPPER_CHEST, + roll = Quaternion(0.0, 1.0, 0.0, 0.0) + }, + { + body = XRBodyTracker.JOINT_NECK, + native = Joint.NECK, + roll = Quaternion(0.0, 1.0, 0.0, 0.0) + }, + { + body = XRBodyTracker.JOINT_HEAD, + native = Joint.HEAD, + roll = Quaternion(0.0, 1.0, 0.0, 0.0) + }, + { + body = XRBodyTracker.JOINT_LEFT_SHOULDER, + native = Joint.LEFT_SHOULDER, + roll = Quaternion(-0.5, 0.5, 0.5, 0.5) + }, + { + body = XRBodyTracker.JOINT_LEFT_UPPER_ARM, + native = Joint.LEFT_UPPER_ARM, + roll = Quaternion(0.5, -0.5, 0.5, 0.5) + }, + { + body = XRBodyTracker.JOINT_LEFT_LOWER_ARM, + native = Joint.LEFT_LOWER_ARM, + roll = Quaternion(-0.7071068, 0.7071068, 0.0, 0.0) + }, + { + body = XRBodyTracker.JOINT_RIGHT_SHOULDER, + native = Joint.RIGHT_SHOULDER, + roll = Quaternion(0.5, 0.5, 0.5, -0.5) + }, + { + body = XRBodyTracker.JOINT_RIGHT_UPPER_ARM, + native = Joint.RIGHT_UPPER_ARM, + roll = Quaternion(0.5, 0.5, -0.5, 0.5) + }, + { + body = XRBodyTracker.JOINT_RIGHT_LOWER_ARM, + native = Joint.RIGHT_LOWER_ARM, + roll = Quaternion(0.7071068, 0.7071068, 0.0, 0.0) + }, + + # Lower Body Joints + { + body = XRBodyTracker.JOINT_LEFT_UPPER_LEG, + native = Joint.LEFT_UPPER_LEG, + roll = Quaternion(1.0, 0.0, 0.0, 0.0) + }, + { + body = XRBodyTracker.JOINT_LEFT_LOWER_LEG, + native = Joint.LEFT_LOWER_LEG, + roll = Quaternion(0.0, 0.0, 1.0, 0.0) + }, + { + body = XRBodyTracker.JOINT_LEFT_FOOT, + native = Joint.LEFT_FOOT, + roll = Quaternion(-0.7071068, 0.0, 0.0, 0.7071068) + }, + { + body = XRBodyTracker.JOINT_RIGHT_UPPER_LEG, + native = Joint.RIGHT_UPPER_LEG, + roll = Quaternion(1.0, 0.0, 0.0, 0.0) + }, + { + body = XRBodyTracker.JOINT_RIGHT_LOWER_LEG, + native = Joint.RIGHT_LOWER_LEG, + roll = Quaternion(0.0, 0.0, 1.0, 0.0) + }, + { + body = XRBodyTracker.JOINT_RIGHT_FOOT, + native = Joint.RIGHT_FOOT, + roll = Quaternion(-0.7071068, 0.0, 0.0, 0.7071068) + }, + + # Left Hand Joints + { + body = XRBodyTracker.JOINT_LEFT_HAND, + native = Joint.LEFT_HAND, + roll = Quaternion(-0.5, 0.5, -0.5, -0.5) + }, + { + body = XRBodyTracker.JOINT_LEFT_WRIST, + native = Joint.LEFT_HAND, + roll = Quaternion(-0.5, 0.5, -0.5, -0.5) + }, + { + body = XRBodyTracker.JOINT_LEFT_THUMB_METACARPAL, + native = Joint.LEFT_THUMB_PROXIMAL, + roll = Quaternion(0.3535534, -0.6123724, 0.6123724, 0.3535534) + }, + { + body = XRBodyTracker.JOINT_LEFT_THUMB_PHALANX_PROXIMAL, + native = Joint.LEFT_THUMB_INTERMEDIATE, + roll = Quaternion(0.3535534, -0.6123724, 0.6123724, 0.3535534) + }, + { + body = XRBodyTracker.JOINT_LEFT_THUMB_PHALANX_DISTAL, + native = Joint.LEFT_THUMB_DISTAL, + roll = Quaternion(0.3535534, -0.6123724, 0.6123724, 0.3535534) + }, + { + body = XRBodyTracker.JOINT_LEFT_INDEX_FINGER_PHALANX_PROXIMAL, + native = Joint.LEFT_INDEX_PROXIMAL, + roll = Quaternion(-0.5, 0.5, -0.5, -0.5) + }, + { + body = XRBodyTracker.JOINT_LEFT_INDEX_FINGER_PHALANX_INTERMEDIATE, + native = Joint.LEFT_INDEX_INTERMEDIATE, + roll = Quaternion(-0.5, 0.5, -0.5, -0.5) + }, + { + body = XRBodyTracker.JOINT_LEFT_INDEX_FINGER_PHALANX_DISTAL, + native = Joint.LEFT_INDEX_DISTAL, + roll = Quaternion(-0.5, 0.5, -0.5, -0.5) + }, + { + body = XRBodyTracker.JOINT_LEFT_MIDDLE_FINGER_PHALANX_PROXIMAL, + native = Joint.LEFT_MIDDLE_PROXIMAL, + roll = Quaternion(-0.5, 0.5, -0.5, -0.5) + }, + { + body = XRBodyTracker.JOINT_LEFT_MIDDLE_FINGER_PHALANX_INTERMEDIATE, + native = Joint.LEFT_MIDDLE_INTERMEDIATE, + roll = Quaternion(-0.5, 0.5, -0.5, -0.5) + }, + { + body = XRBodyTracker.JOINT_LEFT_MIDDLE_FINGER_PHALANX_DISTAL, + native = Joint.LEFT_MIDDLE_DISTAL, + roll = Quaternion(-0.5, 0.5, -0.5, -0.5) + }, + { + body = XRBodyTracker.JOINT_LEFT_RING_FINGER_PHALANX_PROXIMAL, + native = Joint.LEFT_RING_PROXIMAL, + roll = Quaternion(-0.5, 0.5, -0.5, -0.5) + }, + { + body = XRBodyTracker.JOINT_LEFT_RING_FINGER_PHALANX_INTERMEDIATE, + native = Joint.LEFT_RING_INTERMEDIATE, + roll = Quaternion(-0.5, 0.5, -0.5, -0.5) + }, + { + body = XRBodyTracker.JOINT_LEFT_RING_FINGER_PHALANX_DISTAL, + native = Joint.LEFT_RING_DISTAL, + roll = Quaternion(-0.5, 0.5, -0.5, -0.5) + }, + { + body = XRBodyTracker.JOINT_LEFT_PINKY_FINGER_PHALANX_PROXIMAL, + native = Joint.LEFT_LITTLE_PROXIMAL, + roll = Quaternion(-0.5, 0.5, -0.5, -0.5) + }, + { + body = XRBodyTracker.JOINT_LEFT_PINKY_FINGER_PHALANX_INTERMEDIATE, + native = Joint.LEFT_LITTLE_INTERMEDIATE, + roll = Quaternion(-0.5, 0.5, -0.5, -0.5) + }, + { + body = XRBodyTracker.JOINT_LEFT_PINKY_FINGER_PHALANX_DISTAL, + native = Joint.LEFT_LITTLE_DISTAL, + roll = Quaternion(-0.5, 0.5, -0.5, -0.5) + }, + + # Right Hand Joints + { + body = XRBodyTracker.JOINT_RIGHT_HAND, + native = Joint.RIGHT_HAND, + roll = Quaternion(0.5, 0.5, -0.5, 0.5) + }, + { + body = XRBodyTracker.JOINT_RIGHT_WRIST, + native = Joint.RIGHT_HAND, + roll = Quaternion(0.5, 0.5, -0.5, 0.5) + }, + { + body = XRBodyTracker.JOINT_RIGHT_THUMB_METACARPAL, + native = Joint.RIGHT_THUMB_PROXIMAL, + roll = Quaternion(0.3535534, 0.6123724, -0.6123724, 0.3535534) + }, + { + body = XRBodyTracker.JOINT_RIGHT_THUMB_PHALANX_PROXIMAL, + native = Joint.RIGHT_THUMB_INTERMEDIATE, + roll = Quaternion(0.3535534, 0.6123724, -0.6123724, 0.3535534) + }, + { + body = XRBodyTracker.JOINT_RIGHT_THUMB_PHALANX_DISTAL, + native = Joint.RIGHT_THUMB_DISTAL, + roll = Quaternion(0.3535534, 0.6123724, -0.6123724, 0.3535534) + }, + { + body = XRBodyTracker.JOINT_RIGHT_INDEX_FINGER_PHALANX_PROXIMAL, + native = Joint.RIGHT_INDEX_PROXIMAL, + roll = Quaternion(0.5, 0.5, -0.5, 0.5) + }, + { + body = XRBodyTracker.JOINT_RIGHT_INDEX_FINGER_PHALANX_INTERMEDIATE, + native = Joint.RIGHT_INDEX_INTERMEDIATE, + roll = Quaternion(0.5, 0.5, -0.5, 0.5) + }, + { + body = XRBodyTracker.JOINT_RIGHT_INDEX_FINGER_PHALANX_DISTAL, + native = Joint.RIGHT_INDEX_DISTAL, + roll = Quaternion(0.5, 0.5, -0.5, 0.5) + }, + { + body = XRBodyTracker.JOINT_RIGHT_MIDDLE_FINGER_PHALANX_PROXIMAL, + native = Joint.RIGHT_MIDDLE_PROXIMAL, + roll = Quaternion(0.5, 0.5, -0.5, 0.5) + }, + { + body = XRBodyTracker.JOINT_RIGHT_MIDDLE_FINGER_PHALANX_INTERMEDIATE, + native = Joint.RIGHT_MIDDLE_INTERMEDIATE, + roll = Quaternion(0.5, 0.5, -0.5, 0.5) + }, + { + body = XRBodyTracker.JOINT_RIGHT_MIDDLE_FINGER_PHALANX_DISTAL, + native = Joint.RIGHT_MIDDLE_DISTAL, + roll = Quaternion(0.5, 0.5, -0.5, 0.5) + }, + { + body = XRBodyTracker.JOINT_RIGHT_RING_FINGER_PHALANX_PROXIMAL, + native = Joint.RIGHT_RING_PROXIMAL, + roll = Quaternion(0.5, 0.5, -0.5, 0.5) + }, + { + body = XRBodyTracker.JOINT_RIGHT_RING_FINGER_PHALANX_INTERMEDIATE, + native = Joint.RIGHT_RING_INTERMEDIATE, + roll = Quaternion(0.5, 0.5, -0.5, 0.5) + }, + { + body = XRBodyTracker.JOINT_RIGHT_RING_FINGER_PHALANX_DISTAL, + native = Joint.RIGHT_RING_DISTAL, + roll = Quaternion(0.5, 0.5, -0.5, 0.5) + }, + { + body = XRBodyTracker.JOINT_RIGHT_PINKY_FINGER_PHALANX_PROXIMAL, + native = Joint.RIGHT_LITTLE_PROXIMAL, + roll = Quaternion(0.5, 0.5, -0.5, 0.5) + }, + { + body = XRBodyTracker.JOINT_RIGHT_PINKY_FINGER_PHALANX_INTERMEDIATE, + native = Joint.RIGHT_LITTLE_INTERMEDIATE, + roll = Quaternion(0.5, 0.5, -0.5, 0.5) + }, + { + body = XRBodyTracker.JOINT_RIGHT_PINKY_FINGER_PHALANX_DISTAL, + native = Joint.RIGHT_LITTLE_DISTAL, + roll = Quaternion(0.5, 0.5, -0.5, 0.5) + }, +] + +## Mapping of XRFaceTracker blends to VMC face blends +const FACE_BLEND_MAPPING : Array[Dictionary] = [ + # Upper Body Joints + { + face = [ XRFaceTracker.FT_EYE_LOOK_OUT_RIGHT ], + native = [ FaceBlend.EYE_LOOK_OUT_RIGHT ], + }, + { + face = [ XRFaceTracker.FT_EYE_LOOK_IN_RIGHT ], + native = [ FaceBlend.EYE_LOOK_IN_RIGHT ], + }, + { + face = [ XRFaceTracker.FT_EYE_LOOK_UP_RIGHT ], + native = [ FaceBlend.EYE_LOOK_UP_RIGHT ], + }, + { + face = [ XRFaceTracker.FT_EYE_LOOK_DOWN_RIGHT ], + native = [ FaceBlend.EYE_LOOK_DOWN_RIGHT ], + }, + { + face = [ XRFaceTracker.FT_EYE_LOOK_OUT_LEFT ], + native = [ FaceBlend.EYE_LOOK_OUT_LEFT ], + }, + { + face = [ XRFaceTracker.FT_EYE_LOOK_IN_LEFT ], + native = [ FaceBlend.EYE_LOOK_IN_LEFT ], + }, + { + face = [ XRFaceTracker.FT_EYE_LOOK_UP_LEFT ], + native = [ FaceBlend.EYE_LOOK_UP_LEFT ], + }, + { + face = [ XRFaceTracker.FT_EYE_LOOK_DOWN_LEFT ], + native = [ FaceBlend.EYE_LOOK_DOWN_LEFT ], + }, + { + face = [ XRFaceTracker.FT_EYE_CLOSED_RIGHT ], + native = [ FaceBlend.EYE_BLINK_RIGHT ], + }, + { + face = [ XRFaceTracker.FT_EYE_CLOSED_LEFT ], + native = [ FaceBlend.EYE_BLINK_LEFT ], + }, + { + face = [ XRFaceTracker.FT_EYE_CLOSED ], + native = [ FaceBlend.EYE_BLINK_RIGHT, + FaceBlend.EYE_BLINK_LEFT ], + }, + { + face = [ XRFaceTracker.FT_EYE_SQUINT_RIGHT ], + native = [ FaceBlend.EYE_SQUINT_RIGHT ], + }, + { + face = [ XRFaceTracker.FT_EYE_SQUINT_LEFT ], + native = [ FaceBlend.EYE_SQUINT_LEFT ], + }, + { + face = [ XRFaceTracker.FT_EYE_SQUINT ], + native = [ FaceBlend.EYE_SQUINT_RIGHT, + FaceBlend.EYE_SQUINT_LEFT ], + }, + { + face = [ XRFaceTracker.FT_EYE_WIDE_RIGHT ], + native = [ FaceBlend.EYE_WIDE_RIGHT ], + }, + { + face = [ XRFaceTracker.FT_EYE_WIDE_LEFT ], + native = [ FaceBlend.EYE_WIDE_LEFT ], + }, + { + face = [ XRFaceTracker.FT_EYE_WIDE ], + native = [ FaceBlend.EYE_WIDE_RIGHT, + FaceBlend.EYE_WIDE_LEFT ], + }, + { + face = [ XRFaceTracker.FT_BROW_DOWN_RIGHT ], + native = [ FaceBlend.BROW_DOWN_RIGHT ], + }, + { + face = [ XRFaceTracker.FT_BROW_DOWN_LEFT ], + native = [ FaceBlend.BROW_DOWN_LEFT ], + }, + { + face = [ XRFaceTracker.FT_BROW_DOWN ], + native = [ FaceBlend.BROW_DOWN_RIGHT, + FaceBlend.BROW_DOWN_LEFT ], + }, + { + face = [ XRFaceTracker.FT_BROW_OUTER_UP_RIGHT ], + native = [ FaceBlend.BROW_OUTER_UP_RIGHT ], + }, + { + face = [ XRFaceTracker.FT_BROW_OUTER_UP_LEFT ], + native = [ FaceBlend.BROW_OUTER_UP_LEFT ], + }, + { + face = [ XRFaceTracker.FT_NOSE_SNEER_RIGHT ], + native = [ FaceBlend.NOSE_SNEER_RIGHT ], + }, + { + face = [ XRFaceTracker.FT_NOSE_SNEER_LEFT ], + native = [ FaceBlend.NOSE_SNEER_LEFT ], + }, + { + face = [ XRFaceTracker.FT_NOSE_SNEER ], + native = [ FaceBlend.NOSE_SNEER_RIGHT, + FaceBlend.NOSE_SNEER_LEFT ], + }, + { + face = [ XRFaceTracker.FT_CHEEK_SQUINT_RIGHT ], + native = [ FaceBlend.CHEEK_SQUINT_RIGHT ], + }, + { + face = [ XRFaceTracker.FT_CHEEK_SQUINT_LEFT ], + native = [ FaceBlend.CHEEK_SQUINT_LEFT ], + }, + { + face = [ XRFaceTracker.FT_CHEEK_SQUINT ], + native = [ FaceBlend.CHEEK_SQUINT_RIGHT, + FaceBlend.CHEEK_SQUINT_LEFT ], + }, + { + face = [ XRFaceTracker.FT_CHEEK_PUFF, + XRFaceTracker.FT_CHEEK_PUFF_RIGHT, + XRFaceTracker.FT_CHEEK_PUFF_LEFT ], + native = [ FaceBlend.CHEEK_PUFF ], + }, + { + face = [ XRFaceTracker.FT_JAW_OPEN ], + native = [ FaceBlend.JAW_OPEN ], + }, + { + face = [ XRFaceTracker.FT_MOUTH_CLOSED ], + native = [ FaceBlend.MOUTH_CLOSE ], + }, + { + face = [ XRFaceTracker.FT_JAW_RIGHT ], + native = [ FaceBlend.JAW_RIGHT ], + }, + { + face = [ XRFaceTracker.FT_JAW_LEFT ], + native = [ FaceBlend.JAW_LEFT ], + }, + { + face = [ XRFaceTracker.FT_JAW_FORWARD ], + native = [ FaceBlend.JAW_FORWARD ], + }, + { + face = [ XRFaceTracker.FT_LIP_FUNNEL, + XRFaceTracker.FT_LIP_FUNNEL_UPPER, + XRFaceTracker.FT_LIP_FUNNEL_LOWER ], + native = [ FaceBlend.MOUTH_FUNNEL ], + }, + { + face = [ XRFaceTracker.FT_LIP_PUCKER, + XRFaceTracker.FT_LIP_PUCKER_UPPER, + XRFaceTracker.FT_LIP_PUCKER_LOWER ], + native = [ FaceBlend.MOUTH_PUCKER ], + }, + { + face = [ XRFaceTracker.FT_MOUTH_UPPER_UP_RIGHT ], + native = [ FaceBlend.MOUTH_UPPER_UP_RIGHT ], + }, + { + face = [ XRFaceTracker.FT_MOUTH_UPPER_UP_LEFT ], + native = [ FaceBlend.MOUTH_UPPER_UP_LEFT ], + }, + { + face = [ XRFaceTracker.FT_MOUTH_UPPER_UP ], + native = [ FaceBlend.MOUTH_UPPER_UP_RIGHT, + FaceBlend.MOUTH_UPPER_UP_LEFT ], + }, + { + face = [ XRFaceTracker.FT_MOUTH_LOWER_DOWN_RIGHT ], + native = [ FaceBlend.MOUTH_LOWER_DOWN_RIGHT ], + }, + { + face = [ XRFaceTracker.FT_MOUTH_LOWER_DOWN_LEFT ], + native = [ FaceBlend.MOUTH_LOWER_DOWN_LEFT ], + }, + { + face = [ XRFaceTracker.FT_MOUTH_LOWER_DOWN ], + native = [ FaceBlend.MOUTH_LOWER_DOWN_RIGHT, + FaceBlend.MOUTH_LOWER_DOWN_LEFT ], + }, + { + face = [ XRFaceTracker.FT_MOUTH_SMILE_RIGHT ], + native = [ FaceBlend.MOUTH_SMILE_RIGHT ], + }, + { + face = [ XRFaceTracker.FT_MOUTH_SMILE_LEFT ], + native = [ FaceBlend.MOUTH_SMILE_LEFT ], + }, + { + face = [ XRFaceTracker.FT_MOUTH_SMILE ], + native = [ FaceBlend.MOUTH_SMILE_RIGHT, + FaceBlend.MOUTH_SMILE_LEFT ], + }, + { + face = [ XRFaceTracker.FT_MOUTH_FROWN_RIGHT ], + native = [ FaceBlend.MOUTH_FROWN_RIGHT ], + }, + { + face = [ XRFaceTracker.FT_MOUTH_FROWN_LEFT ], + native = [ FaceBlend.MOUTH_FROWN_LEFT ], + }, + { + face = [ XRFaceTracker.FT_MOUTH_STRETCH_RIGHT ], + native = [ FaceBlend.MOUTH_STRETCH_RIGHT ], + }, + { + face = [ XRFaceTracker.FT_MOUTH_STRETCH_LEFT ], + native = [ FaceBlend.MOUTH_STRETCH_LEFT ], + }, + { + face = [ XRFaceTracker.FT_MOUTH_STRETCH ], + native = [ FaceBlend.MOUTH_STRETCH_RIGHT, + FaceBlend.MOUTH_STRETCH_LEFT ], + }, + { + face = [ XRFaceTracker.FT_MOUTH_DIMPLE_RIGHT ], + native = [ FaceBlend.MOUTH_DIMPLE_RIGHT ], + }, + { + face = [ XRFaceTracker.FT_MOUTH_DIMPLE_LEFT ], + native = [ FaceBlend.MOUTH_DIMPLE_LEFT ], + }, + { + face = [ XRFaceTracker.FT_MOUTH_DIMPLE ], + native = [ FaceBlend.MOUTH_DIMPLE_RIGHT, + FaceBlend.MOUTH_DIMPLE_LEFT ], + }, + { + face = [ XRFaceTracker.FT_MOUTH_PRESS_RIGHT ], + native = [ FaceBlend.MOUTH_PRESS_RIGHT ], + }, + { + face = [ XRFaceTracker.FT_MOUTH_PRESS_LEFT ], + native = [ FaceBlend.MOUTH_PRESS_LEFT ], + }, + { + face = [ XRFaceTracker.FT_MOUTH_PRESS ], + native = [ FaceBlend.MOUTH_PRESS_RIGHT, + FaceBlend.MOUTH_PRESS_LEFT ], + }, +] diff --git a/addons/godot_vmc_tracker/vmc_plugin.gd b/addons/godot_vmc_tracker/vmc_plugin.gd new file mode 100644 index 0000000..2b1fd5e --- /dev/null +++ b/addons/godot_vmc_tracker/vmc_plugin.gd @@ -0,0 +1,45 @@ +extends Node + + +## VMC Plugin Node +## +## This node provides a VMC tracker as a plugin autoload singleton. + + +# Tracker source +var _source : VMCSource + + +# On entering the scene-tree, construct the tracker source and start listening +# for incoming packets. +func _enter_tree() -> void: + # Get the face tracker name + var face_tracker_name : String = ProjectSettings.get_setting( + "godot_vmc_tracker/tracking/face_tracker_name", + "/vmc/head") + + # Get the body tracker name + var body_tracker_name : String = ProjectSettings.get_setting( + "godot_vmc_tracker/tracking/body_tracker_name", + "/vmc/body") + + # Get the position mode + var position_mode = ProjectSettings.get_setting( + "godot_vmc_tracker/tracking/position_mode", + 0) + + # Get the UDP port number + var udp_listener_port : int = ProjectSettings.get_setting( + "godot_vmc_tracker/network/udp_listener_port", + 39539) + + _source = VMCSource.new( + face_tracker_name, + body_tracker_name, + position_mode, + udp_listener_port) + + +# On frame processing, poll the tracker source for updates. +func _process(_delta: float) -> void: + _source.poll() diff --git a/addons/godot_vmc_tracker/vmc_source.gd b/addons/godot_vmc_tracker/vmc_source.gd new file mode 100644 index 0000000..1b424c3 --- /dev/null +++ b/addons/godot_vmc_tracker/vmc_source.gd @@ -0,0 +1,243 @@ +class_name VMCSource +extends Object + + +## VMC Tracker Script +## +## This script processes VMC packets into XRFaceTracker and XRBodyTracker data +## for driving avatars. + + +## Enumeration of position modes +enum PositionMode { + FREE, ## Free movement + CALIBRATE, ## Calibrate horizontal position on the first frame + LOCKED ## Lock horizontal position +} + + +## Body tracking flags +const BODY_TRACKING := \ + XRBodyTracker.BODY_FLAG_UPPER_BODY_SUPPORTED | \ + XRBodyTracker.BODY_FLAG_LOWER_BODY_SUPPORTED | \ + XRBodyTracker.BODY_FLAG_HANDS_SUPPORTED + +## Joint tracking flags +const JOINT_TRACKING := \ + XRBodyTracker.JOINT_FLAG_ORIENTATION_TRACKED | \ + XRBodyTracker.JOINT_FLAG_ORIENTATION_VALID | \ + XRBodyTracker.JOINT_FLAG_POSITION_TRACKED | \ + XRBodyTracker.JOINT_FLAG_POSITION_VALID + + +# OSC reader instance +var _osc_reader : OSCReader = OSCReader.new() + +# Face tracker instance to publish tracking data +var _face_tracker : XRFaceTracker = XRFaceTracker.new() + +# Body tracker instance to publish tracking data +var _body_tracker : XRBodyTracker = XRBodyTracker.new() + +# Position mode +var _position_mode : PositionMode = PositionMode.FREE + +# Array of joint relative positions +var _rel_positions : Array[Vector3] = [] + +# Array of joint relative rotations +var _rel_rotations : Array[Quaternion] = [] + +# Array of joint absolute positions +var _abs_positions : Array[Vector3] = [] + +# Array of joint absolute rotations +var _abs_rotations : Array[Quaternion] = [] + +# Array of face blends +var _face_blends : Array[float] = [] + +# Position calibration +var _position_calibration : Vector3 = Vector3.ZERO + +# True if new joint data available +var _new_joints : bool = false + +# True if new face blend data available +var _new_face_blends : bool = false + +# Calibrated flag +var _position_calibrated : bool = false + + +# On initialization, construct and register the face and body trackers and +# start listening for incoming packets. +func _init( + face_tracker_name : String, + body_tracker_name : String, + position_mode : int, + udp_listener_port : int) -> void: + + # Fill the position and rotation arrays + _rel_positions.resize(VMCBody.Joint.COUNT) + _rel_rotations.resize(VMCBody.Joint.COUNT) + _abs_positions.resize(VMCBody.Joint.COUNT) + _abs_rotations.resize(VMCBody.Joint.COUNT) + _rel_positions.fill(Vector3.ZERO) + _rel_rotations.fill(Quaternion.IDENTITY) + _abs_positions.fill(Vector3.ZERO) + _abs_rotations.fill(Quaternion.IDENTITY) + + # Fill the face blend array + _face_blends.resize(VMCBody.FaceBlend.COUNT) + _face_blends.fill(0.0) + + # Register the face tracker + XRServer.add_face_tracker(face_tracker_name, _face_tracker) + + # Register the body tracker + XRServer.add_body_tracker(body_tracker_name, _body_tracker) + + # Save the position mode + _position_mode = position_mode + + + # Start listening for VMC packets + _osc_reader.on_osc_packet.connect(_on_osc_packet) + _osc_reader.listen(udp_listener_port) + + +# Poll for incoming packets +func poll() -> void: + _osc_reader.poll() + + +# Handle received OSC packet data +func _on_osc_packet(data : Array) -> void: + # Process all OSC entries + for entry in data: + match entry[0]: + "/VMC/Ext/Bone/Pos": + _on_vmc_ext_bone_pos(entry) + "/VMC/Ext/Blend/Val": + _on_vmc_ext_blend_val(entry) + + # Process new joint data + if _new_joints: + _new_joints = false + _process_joints() + + # Process new face blends + if _new_face_blends: + _new_face_blends = false + _process_face_blends() + + +# Handle a VMC bone position +func _on_vmc_ext_bone_pos(entry : Array) -> void: + # Get the VMC joint + var joint : VMCBody.Joint = VMCBody.JOINT_NAMES.get(entry[1], -1) + if joint < 0: + return + + # Save the relative positions and rotations + var pos := Vector3(entry[2], entry[3], -entry[4]) + var rot := Quaternion(entry[5], entry[6], -entry[7], -entry[8]) + + # If hips then consider position calibration + if joint == VMCBody.Joint.HIPS: + match _position_mode: + PositionMode.CALIBRATE: + # Calibrate on first position + if not _position_calibrated: + _position_calibrated = true + _position_calibration = pos.slide(Vector3.UP) + + # Apply calibration + pos -= _position_calibration + + PositionMode.LOCKED: + # Project position to vertical axis + pos = pos.project(Vector3.UP) + + # Save the joint + _rel_positions[joint] = pos + _rel_rotations[joint] = rot + _new_joints = true + + +# Handle a VMC face blend value +func _on_vmc_ext_blend_val(entry : Array) -> void: + # Get the VMC face blend + var blend : VMCBody.FaceBlend = VMCBody.FACE_BLEND_NAMES.get(entry[1], -1) + if blend < 0: + return + + # Save the face blend + _face_blends[blend] = entry[2] + _new_face_blends = true + + +# Process VMC joint data into XRBodyTracker data +func _process_joints() -> void: + # Iterate over the joints + for joint in VMCBody.Joint.COUNT: + # Get the joint information and relative location + var parent_joint := VMCBody.JOINT_PARENT[joint] + var pos := _rel_positions[joint] + var rot := _rel_rotations[joint] + + # If child-joint then convert relative to absolute + if parent_joint >= 0: + var parent_pos := _abs_positions[parent_joint] + var parent_rot := _abs_rotations[parent_joint] + pos = parent_pos + parent_rot * pos + rot = parent_rot * rot + + # Save absolute position + _abs_positions[joint] = pos + _abs_rotations[joint] = rot + + # Apply to the XRBodyTracker + for joint in VMCBody.JOINT_MAPPING: + var body : XRBodyTracker.Joint = joint["body"] + var vmc : VMCBody.Joint = joint["native"] + var roll : Quaternion = joint["roll"] + + # Set the joint transform + _body_tracker.set_joint_transform( + body, + Transform3D( + Basis(_abs_rotations[vmc] * roll), + _abs_positions[vmc])) + + # Set the joint flags + _body_tracker.set_joint_flags(body, JOINT_TRACKING) + + # Calculate and set the root joint under the hips + var root := _body_tracker.get_joint_transform(XRBodyTracker.JOINT_HIPS) + root.basis = Basis.IDENTITY + root.origin = root.origin.slide(Vector3.UP) + _body_tracker.set_joint_transform(XRBodyTracker.JOINT_ROOT, root) + _body_tracker.set_joint_flags(XRBodyTracker.JOINT_ROOT, JOINT_TRACKING) + + # Indicate we are tracking the body + _body_tracker.body_flags = BODY_TRACKING + _body_tracker.has_tracking_data = true + + +# Process VMC face blend data into XRFaceTracker +func _process_face_blends() -> void: + # Apply to the XRFaceTracker + for blend in VMCBody.FACE_BLEND_MAPPING: + var face : Array = blend["face"] + var vmc : Array = blend["native"] + + var weight := 0.0 + for v in vmc: + weight += _face_blends[v] + weight /= vmc.size() + + # Set the face blend weight + for f in face: + _face_tracker.set_blend_shape(f, weight) diff --git a/addons/godot_vmc_tracker/vmc_tracker.gd b/addons/godot_vmc_tracker/vmc_tracker.gd new file mode 100644 index 0000000..d003629 --- /dev/null +++ b/addons/godot_vmc_tracker/vmc_tracker.gd @@ -0,0 +1,41 @@ +class_name VMCTracker +extends Node + + +## VMC Tracker Node +## +## This node provides a VMC tracker as a scene-tree node. It may also +## be instantiated as an autoload to provide for multiple trackers on different +## ports. + + +## Face tracker name +@export var face_tracker_name : String = "/mvn/head" + +## Body tracker name +@export var body_tracker_name : String = "/mvn/body" + +## Position mode +@export_enum("Free", "Calibrate", "Locked") var position_mode : int = 0 + +## UDP listener port +@export var udp_listener_port : int = 7004 + + +# Tracker source +var _source : VMCSource + + +# On entering the scene-tree, construct the tracker source and start listening +# for incoming packets. +func _enter_tree() -> void: + _source = VMCSource.new( + face_tracker_name, + body_tracker_name, + position_mode, + udp_listener_port) + + +# On frame processing, poll the tracker source for updates. +func _process(_delta: float) -> void: + _source.poll() diff --git a/addons/godot_vmc_tracker/vmc_tracker.tscn b/addons/godot_vmc_tracker/vmc_tracker.tscn new file mode 100644 index 0000000..a1b1460 --- /dev/null +++ b/addons/godot_vmc_tracker/vmc_tracker.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3 uid="uid://dvnmxtkucajqw"] + +[ext_resource type="Script" path="res://addons/godot_vmc_tracker/vmc_tracker.gd" id="1_w1cjd"] + +[node name="VmcTracker" type="Node"] +script = ExtResource("1_w1cjd") diff --git a/icon.svg b/icon.svg new file mode 100644 index 0000000..3fe4f4a --- /dev/null +++ b/icon.svg @@ -0,0 +1 @@ + diff --git a/icon.svg.import b/icon.svg.import new file mode 100644 index 0000000..c70d3f3 --- /dev/null +++ b/icon.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://d2w8o4j2irgdk" +path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://icon.svg" +dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.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=false +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 diff --git a/load_test.gd b/load_test.gd new file mode 100644 index 0000000..ef52ef9 --- /dev/null +++ b/load_test.gd @@ -0,0 +1,40 @@ +extends Node3D + + +@export var rpm_settings : RpmSettings + + +# Current avatar +var _avatar : Node3D + + +# Called when the node enters the scene tree for the first time. +func _ready() -> void: + RpmLoader.load_complete.connect(_on_load_complete) + RpmLoader.load_failed.connect(_on_load_failed) + + +func _on_load_button_pressed() -> void: + if %AvatarID.text == "": + return + + # Queue the download + %Status.text = "Downloading ..." + RpmLoader.load_web(%AvatarID.text, rpm_settings) + + +func _on_load_complete(_id : String, avatar : Node3D) -> void: + # Free the old avatar + if _avatar: + _avatar.queue_free() + + # Add the avatar to this scene + _avatar = avatar + add_child(_avatar) + %Status.text = "Success" + + +func _on_load_failed(_id : String) -> void: + %Status.text = "Load Failed" + + diff --git a/load_test.tscn b/load_test.tscn new file mode 100644 index 0000000..0cdd854 --- /dev/null +++ b/load_test.tscn @@ -0,0 +1,86 @@ +[gd_scene load_steps=7 format=3 uid="uid://cq8p13kfy4cuf"] + +[ext_resource type="Script" path="res://load_test.gd" id="1_ha8ec"] +[ext_resource type="Script" path="res://addons/godot_rpm_avatar/rpm_settings.gd" id="2_fq1tk"] + +[sub_resource type="Resource" id="Resource_haakg"] +script = ExtResource("2_fq1tk") +body_tracker = "/vmc/body" +face_tracker = "/vmc/head" +quality = 1 + +[sub_resource type="ProceduralSkyMaterial" id="ProceduralSkyMaterial_mxfgc"] +sky_horizon_color = Color(0.64625, 0.65575, 0.67075, 1) +ground_horizon_color = Color(0.64625, 0.65575, 0.67075, 1) + +[sub_resource type="Sky" id="Sky_gw6ve"] +sky_material = SubResource("ProceduralSkyMaterial_mxfgc") + +[sub_resource type="Environment" id="Environment_r8g28"] +background_mode = 2 +sky = SubResource("Sky_gw6ve") +tonemap_mode = 2 +glow_enabled = true + +[node name="LoadTest" type="Node3D"] +script = ExtResource("1_ha8ec") +rpm_settings = SubResource("Resource_haakg") + +[node name="WorldEnvironment" type="WorldEnvironment" parent="."] +environment = SubResource("Environment_r8g28") + +[node name="DirectionalLight3D" type="DirectionalLight3D" parent="."] +transform = Transform3D(-0.866023, -0.433016, 0.250001, 0, 0.499998, 0.866027, -0.500003, 0.749999, -0.43301, 0, 0, 0) +shadow_enabled = true + +[node name="Camera3D" type="Camera3D" parent="."] +transform = Transform3D(-1, 4.37114e-08, -7.57104e-08, 0, 0.866026, 0.5, 8.74228e-08, 0.5, -0.866026, 0, 1.8, -1) + +[node name="PanelContainer" type="PanelContainer" parent="."] +offset_left = 10.0 +offset_top = 10.0 +offset_right = 498.0 +offset_bottom = 78.0 + +[node name="MarginContainer" type="MarginContainer" parent="PanelContainer"] +layout_mode = 2 +theme_override_constants/margin_left = 5 +theme_override_constants/margin_top = 5 +theme_override_constants/margin_right = 5 +theme_override_constants/margin_bottom = 5 + +[node name="HBoxContainer" type="HBoxContainer" parent="PanelContainer/MarginContainer"] +layout_mode = 2 + +[node name="Label" type="Label" parent="PanelContainer/MarginContainer/HBoxContainer"] +custom_minimum_size = Vector2(150, 0) +layout_mode = 2 +text = "ReadyPlayerMe +VRM Avatar " +horizontal_alignment = 1 + +[node name="VBoxContainer" type="VBoxContainer" parent="PanelContainer/MarginContainer/HBoxContainer"] +layout_mode = 2 + +[node name="HBoxContainer" type="HBoxContainer" parent="PanelContainer/MarginContainer/HBoxContainer/VBoxContainer"] +layout_mode = 2 + +[node name="AvatarID" type="LineEdit" parent="PanelContainer/MarginContainer/HBoxContainer/VBoxContainer/HBoxContainer"] +unique_name_in_owner = true +custom_minimum_size = Vector2(240, 0) +layout_mode = 2 +text = "65fa409029044c117cbd3e3c" + +[node name="LoadButton" type="Button" parent="PanelContainer/MarginContainer/HBoxContainer/VBoxContainer/HBoxContainer"] +custom_minimum_size = Vector2(80, 0) +layout_mode = 2 +size_flags_horizontal = 4 +text = "Load" + +[node name="Status" type="Label" parent="PanelContainer/MarginContainer/HBoxContainer/VBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +horizontal_alignment = 2 + +[connection signal="gui_input" from="PanelContainer/MarginContainer/HBoxContainer/VBoxContainer/HBoxContainer/AvatarID" to="." method="_on_avatar_id_gui_input"] +[connection signal="pressed" from="PanelContainer/MarginContainer/HBoxContainer/VBoxContainer/HBoxContainer/LoadButton" to="." method="_on_load_button_pressed"] diff --git a/project.godot b/project.godot new file mode 100644 index 0000000..8da0372 --- /dev/null +++ b/project.godot @@ -0,0 +1,33 @@ +; Engine configuration file. +; It's best edited using the editor UI and not directly, +; since the parameters that go here are not all obvious. +; +; Format: +; [section] ; section goes between [] +; param=value ; assign values to parameters + +config_version=5 + +[application] + +config/name="Godot ReadyPlayerMe Avatar" +run/main_scene="res://load_test.tscn" +config/features=PackedStringArray("4.3", "Mobile") +config/icon="res://icon.svg" + +[autoload] + +RpmLoader="*res://addons/godot_rpm_avatar/rpm_loader.gd" +VmcPlugin="*res://addons/godot_vmc_tracker/vmc_plugin.gd" + +[editor_plugins] + +enabled=PackedStringArray("res://addons/godot_rpm_avatar/plugin.cfg", "res://addons/godot_vmc_tracker/plugin.cfg") + +[godot_vmc_tracker] + +tracking/position_mode=1 + +[rendering] + +renderer/rendering_method="mobile"