Skip to content

Commit

Permalink
Merge pull request #175 from johncosfm/main
Browse files Browse the repository at this point in the history
script for exporting facial animation and setting face poses
  • Loading branch information
Simarilius-uk authored Sep 20, 2024
2 parents a520374 + 7775445 commit 4348b36
Showing 1 changed file with 359 additions and 0 deletions.
359 changes: 359 additions & 0 deletions i_scene_cp77_gltf/resources/scripts/cyberpunk_facial_exporter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,359 @@
#all of these should be json
facialsetupFile = r"F:\rip\cyberpunk\wkproject\kerry_face\kerryface\source\raw\base\characters\main_npc\kerry_eurodyne\h0_001_ma_c__kerry_eurodyne\h0_001_ma_c__kerry_eurodyne_old_rigsetup.facialsetup.json"
rigFile = r"F:\rip\cyberpunk\wkproject\kerry_face\kerryface\source\raw\base\characters\main_npc\kerry_eurodyne\h0_001_ma_c__kerry_eurodyne\h0_001_ma_c__kerry_eurodyne_old_skeleton.rig.json"
#only scenerid is supported rn but it should be pretty straightforward to load from .anims, just need to change where ApplyFacialSequenceFromeSceneRid looks for the animation data
animFile = r"F:\rip\cyberpunk\wkproject\kerry_face\kerryface\source\raw\base\animations\quest\side_quests\sq011\sq011_10_concert\rid\sq011_10_concert__kerry_performance.scenerid.json"

import bpy
import mathutils
import math
import base64
import copy

facialsetup = None
rig = None
def Init():
ReadFiles()
CreateArmature() #creates an armature by the name of facialsetup_armature from the rig file unless one alread exists

ApplyFacialSequenceFromeSceneRid("app_kerry_eurodyne_old__kerry_eurodyne_controlRig", "sq011_10_concert__kerry_performance_anim_sn5")
#third argument tells it to stich this onto the end of the previous sequence, useful for scenerid where stuff tends to be cut up into chunks
ApplyFacialSequenceFromeSceneRid("app_kerry_eurodyne_old__kerry_eurodyne_controlRig", "sq011_10_concert__kerry_performance_anim_sn6", bpy.context.scene.frame_end)
ApplyFacialSequenceFromeSceneRid("app_kerry_eurodyne_old__kerry_eurodyne_controlRig", "sq011_10_concert__kerry_performance_anim_sn7", bpy.context.scene.frame_end)
ApplyFacialSequenceFromeSceneRid("app_kerry_eurodyne_old__kerry_eurodyne_controlRig", "sq011_10_concert__kerry_performance_anim_sn8", bpy.context.scene.frame_end)
bpy.context.scene.frame_start = 1
#do this to apply a set of individual poses
# trackSettings = {}
# trackSettings["eye_r_blink"] = 1 #scale, can be greater than 1 or negative, i havent specifically tried it but negative probably doesnt apply rotations correctly
# trackSettings["eye_l_blink"] = 1 #if you want to fix applying rotations for negative scales the place to do that is in ScaleCoordSet
# coordset = InitCoordSet()
# for trackName in trackSettings.keys():
# CombineCoordSet(coordset, ScaleCoordSet(GetBoneTransformsForTrack(trackName), trackSettings[trackName]))
# ApplyCorrectiveShapes(coordset, trackSettings) #this doesnt actually do anything rn lol
# ApplyCoordSet(coordset) #give this a second argument with a frame number to keyframe instead of just set

def ReadFiles():
print("reading rig...")
jsonfile = open(rigFile, "r")
exec("rig = "+jsonfile.read().replace("null", "None").replace("true", "True").replace("false", "False"), globals())
jsonfile.close()

print("reading facialsetup...")
jsonfile = open(facialsetupFile, "r")
exec("facialsetup = "+jsonfile.read().replace("null", "None").replace("true", "True").replace("false", "False"), globals())
jsonfile.close()

print("reading anim...")
jsonfile = open(animFile, "r")
exec("anim = "+jsonfile.read().replace("null", "None").replace("true", "True").replace("false", "False"), globals())
jsonfile.close()
print("done")

def CreateArmature():
if "facialsetup_armature" in bpy.data.objects:
return
armature = bpy.data.objects.new("facialsetup_armature", bpy.data.armatures.new("facialsetup_armature"))
bpy.context.scene.collection.objects.link(armature)
bpy.context.view_layer.objects.active = armature
bpy.ops.object.mode_set(mode='EDIT')
for i in range(0, len(rig["Data"]["RootChunk"]["boneNames"])):
bone = armature.data.edit_bones.new(rig["Data"]["RootChunk"]["boneNames"][i]["$value"])
bone.head = (0,0,0)
bone.tail = (0,0,0.01)
for i in range(0, len(rig["Data"]["RootChunk"]["boneNames"])):
child = rig["Data"]["RootChunk"]["boneNames"][i]["$value"]
parentindex = rig["Data"]["RootChunk"]["boneParentIndexes"][i]
if parentindex == -1:
continue
parent = rig["Data"]["RootChunk"]["boneNames"][parentindex]["$value"]
armature.data.edit_bones[child].parent = armature.data.edit_bones[parent]
bpy.ops.object.mode_set(mode='OBJECT')
for i in range(0, len(rig["Data"]["RootChunk"]["boneNames"])):
bone = armature.pose.bones[rig["Data"]["RootChunk"]["boneNames"][i]["$value"]]
transform = rig["Data"]["RootChunk"]["boneTransforms"][i]
bone.location = mathutils.Vector((transform["Translation"]["X"], transform["Translation"]["Y"], transform["Translation"]["Z"]))
bone.rotation_quaternion = (transform["Rotation"]["r"], transform["Rotation"]["i"], transform["Rotation"]["j"], transform["Rotation"]["k"])
bpy.context.view_layer.objects.active = armature
bpy.ops.object.mode_set(mode='POSE')
bpy.ops.pose.armature_apply()
bpy.ops.object.mode_set(mode='OBJECT')
armature.rotation_euler = mathutils.Euler((math.radians(-90),0,0))

def ApplyFacialSequenceFromeSceneRid(actorSignature, sequence, offset=0):
#find actor
actor = None
for i in range(0, len(anim["Data"]["RootChunk"]["actors"])):
if anim["Data"]["RootChunk"]["actors"][i]["tag"]["signature"]["$value"] == actorSignature:
actor = anim["Data"]["RootChunk"]["actors"][i]
break
if not actor:
print("couldnt find actor "+actorSignature)
return
#find anim data
animdata = None
for i in range(0, len(actor["facialAnimations"])):
if actor["facialAnimations"][i]["animation"]["Data"]["name"]["$value"] == sequence:
animdata = actor["facialAnimations"][i]["animation"]["Data"]
break
if not animdata:
print("couldnt find sequence "+sequence)
return
#read buffer
trackAnimation = {}
br = BinaryReader(base64.b64decode(animdata["animBuffer"]["Data"]["defferedBuffer"]["Bytes"]), Endian.LITTLE)
#dont give a shit about these
for i in range(0,animdata["animBuffer"]["Data"]["numAnimKeys"]):
br.read_uint16()
br.read_uint16()
br.read_uint16()
br.read_uint16()
br.read_uint16()
for i in range(0,animdata["animBuffer"]["Data"]["numAnimKeysRaw"]):
br.read_uint16()
br.read_uint16()
br.read_uint32()
br.read_uint32()
br.read_uint32()
for i in range(0,animdata["animBuffer"]["Data"]["numConstAnimKeys"]):
br.read_uint16()
br.read_uint16()
br.read_uint32()
br.read_uint32()
br.read_uint32()
#track info
for i in range(0,animdata["animBuffer"]["Data"]["numTrackKeys"]):
time = (br.read_uint16() / 65535) * animdata["animBuffer"]["Data"]["duration"]
idx = br.read_uint16();
value = br.read_float()
if not idx in trackAnimation:
trackAnimation[idx] = {}
trackAnimation[idx][time] = value
for i in range(0,animdata["animBuffer"]["Data"]["numConstTrackKeys"]):
idx = br.read_uint16();
time = (br.read_uint16() / 65535) * animdata["animBuffer"]["Data"]["duration"]
value = br.read_uint32() / 4294967295
if not idx in trackAnimation:
trackAnimation[idx] = {}
trackAnimation[idx][time] = value
bpy.context.scene.render.fps = 30
bpy.context.scene.frame_start = 1 + offset
bpy.context.scene.frame_end = offset + round(animdata["animBuffer"]["Data"]["duration"] / (1 / 30) + 0.5)
for frame in range(bpy.context.scene.frame_start, bpy.context.scene.frame_end + 1):
frameTime = (frame - offset) * (1/30)
frameSettings = {}
for trackIndex in trackAnimation.keys():
#calc blend
startTime = None
endTime = None
for time in trackAnimation[trackIndex].keys():
if time < frameTime:
startTime = time
else:
endTime = time
break
if not startTime and not endTime:
continue
if not startTime:
startTime = endTime
if not endTime:
endTime = startTime
blend = 0
timeDiff = endTime - startTime
if timeDiff > 0:
blend = (frameTime - startTime) / timeDiff
trackValue = trackAnimation[trackIndex][startTime] * (1 - blend) + trackAnimation[trackIndex][endTime] * blend
if trackValue == 0:
continue
frameSettings[rig["Data"]["RootChunk"]["trackNames"][trackIndex]["$value"]] = trackValue
frameCoordSet = InitCoordSet()
for trackName in frameSettings.keys():
CombineCoordSet(frameCoordSet, ScaleCoordSet(GetBoneTransformsForTrack(trackName), frameSettings[trackName]))
ApplyCorrectiveShapes(frameCoordSet, frameSettings)
print(frame)
ApplyCoordSet(frameCoordSet, frame)

def ApplyCorrectiveShapes(coordset, trackSettings):
#corrective shapes arent implemented yet
return coordset

def GetBoneTransformsForTrack(track):
#print("getting bone transforms for track "+track)
#find track index
trackIndex = -1
for i in range(0,len(rig["Data"]["RootChunk"]["trackNames"])):
if rig["Data"]["RootChunk"]["trackNames"][i]["$value"] == track:
trackIndex = i
break
if trackIndex == -1:
print("couldnt find track in rig file")
return
#print("trackIndex = "+str(trackIndex))
coordset = {}
#find face part
#print("finding face parts")
for part in ["Face", "Tongue", "Eyes"]:
found = False
for v in facialsetup["Data"]["RootChunk"]["bakedData"]["Data"][part]["UpperLowerFace"]:
if v["Track"] == trackIndex:
found = True
if not found:
continue
#print("track found in face part " + part)
#convert track index to face part specific pose index
poseIndex = -1
for i in range(0,len(facialsetup["Data"]["RootChunk"]["bakedData"]["Data"][part]["AllMainPoses"])):
if facialsetup["Data"]["RootChunk"]["bakedData"]["Data"][part]["AllMainPoses"][i]["Track"] == trackIndex:
poseIndex = i
break
if poseIndex == -1:
print("couldnt find pose index for track")
return
#print("poseIndex = "+str(poseIndex))
#find transform set
poseDef = facialsetup["Data"]["RootChunk"]["mainPosesData"]["Data"][part]["Poses"][poseIndex]
startTransform = poseDef["TransformIdx"]
stopTransform = startTransform + poseDef["NumTransforms"]
#print("startTransform = "+str(startTransform))
#print("endTransform = "+str(stopTransform))
#collect transform info
for transformIndex in range(startTransform, stopTransform):
transform = facialsetup["Data"]["RootChunk"]["mainPosesData"]["Data"][part]["Transforms"][transformIndex]
bone = rig["Data"]["RootChunk"]["boneNames"][transform["Bone"]]["$value"]
#print(str(transformIndex)+": "+bone)
pos = mathutils.Vector((transform["Translation"]["X"], transform["Translation"]["Y"], transform["Translation"]["Z"]))
#print(" pos:"+FormatVectorForString(pos))
rot = mathutils.Quaternion((transform["Rotation"]["r"], transform["Rotation"]["i"], transform["Rotation"]["j"], transform["Rotation"]["k"]))
CombineCoordSet(coordset,{bone.lower(): {"pos" : pos, "rot" : rot}})
return coordset

def InitCoordSet():
coordSet = {}
for bone in rig["Data"]["RootChunk"]["boneNames"]:
coordSet[bone["$value"].lower()] = {"pos" : mathutils.Vector((0, 0, 0)), "rot" : mathutils.Quaternion()}
return coordSet

def CombineCoordSet(a,b):
for bone in b.keys():
if bone in a:
a[bone]["pos"] += b[bone]["pos"]
a[bone]["rot"].rotate(b[bone]["rot"])
else:
a[bone] = b[bone]
return a

def ScaleCoordSet(set, scale):
for bone in set.keys():
set[bone]["pos"] *= scale
rotscale = scale
newrot = mathutils.Quaternion()
while rotscale > 0: #slerp only allows a range 0-1 so gotta do this shit, doesnt support negative scale tho
newrot.rotate(set[bone]["rot"].slerp(mathutils.Quaternion(), max(0, min(1 - scale, 1))))
rotscale -= 1
set[bone]["rot"] = newrot
return set

def ApplyCoordSet(set, frame=None):
if frame:
bpy.context.scene.frame_set(frame)
if not set:
return
for posebone in bpy.data.objects["facialsetup_armature"].pose.bones:
if posebone.name.lower() in set:
posebone.location = set[posebone.name.lower()]["pos"]
posebone.rotation_mode = "QUATERNION"
posebone.rotation_quaternion = set[posebone.name.lower()]["rot"]
if frame:
posebone.keyframe_insert("location", frame=frame)
posebone.keyframe_insert("rotation_quaternion", frame=frame)


def FormatVectorForString(vec):
str = ""
if not format(vec.x, '.4f').startswith("-"):
str += " "
str += format(vec.x, '.4f')+","
if not format(vec.y, '.4f').startswith("-"):
str += " "
str += format(vec.y, '.4f')+","
if not format(vec.z, '.4f').startswith("-"):
str += " "
str += format(vec.z, '.4f')
return str

#gutted binary reader stuff that im sticking in here just for the sake of only portability
#https://github.com/K0lb3/binaryreader
#__author__ = "SutandoTsukai181"
#__copyright__ = "Copyright 2021, SutandoTsukai181"
#__license__ = "MIT"
#__version__ = "1.4.3"
import struct
from contextlib import contextmanager
from enum import Flag, IntEnum
from typing import Tuple, Union
FMT = dict()
for c in ["b", "B", "s"]:
FMT[c] = 1
for c in ["h", "H", "e"]:
FMT[c] = 2
for c in ["i", "I", "f"]:
FMT[c] = 4
for c in ["q", "Q"]:
FMT[c] = 8
class Endian(Flag):
LITTLE = False
BIG = True
class Whence(IntEnum):
BEGIN = 0
CUR = 1
END = 2
class BrStruct:
def __init__(self) -> None:
pass
def __br_read__(self, br: 'BinaryReader', *args) -> None:
pass
def __br_write__(self, br: 'BinaryReader', *args) -> None:
pass
class BinaryReader:
__buf: bytearray
__idx: int
__endianness: Endian
__encoding: str
def __init__(self, buffer: bytearray = bytearray(), endianness: Endian = Endian.LITTLE, encoding='utf-8'):
self.__buf = bytearray(buffer)
self.__endianness = endianness
self.__idx = 0
self.set_encoding(encoding)
def __past_eof(self, index: int) -> bool:
return index > self.size()
def past_eof(self) -> bool:
return self.__past_eof(self.pos())
def size(self) -> int:
return len(self.__buf)
def set_endian(self, endianness: Endian) -> None:
self.__endianness = endianness
def set_encoding(self, encoding: str) -> None:
str.encode('', encoding)
self.__encoding = encoding
def __read_type(self, format: str, count=1):
i = self.__idx
new_offset = self.__idx + (FMT[format] * count)
end = ">" if self.__endianness else "<"
if self.__past_eof(new_offset):
raise Exception(
'BinaryReader Error: cannot read farther than buffer length.')
self.__idx = new_offset
return struct.unpack_from(end + str(count) + format, self.__buf, i)
def read_uint32(self, count=None) -> Union[int, Tuple[int]]:
if count is not None:
return self.__read_type("I", count)
return self.__read_type("I")[0]
def read_uint16(self, count=None) -> Union[int, Tuple[int]]:
if count is not None:
return self.__read_type("H", count)
return self.__read_type("H")[0]
def read_float(self, count=None) -> Union[float, Tuple[float]]:
if count is not None:
return self.__read_type("f", count)
return self.__read_type("f")[0]

Init()

0 comments on commit 4348b36

Please sign in to comment.