Skip to content

Instantly share code, notes, and snippets.

@steveway
Last active September 6, 2021 11:08
Show Gist options
  • Save steveway/ae902966384de2bd4d2e81c5cc0af5ed to your computer and use it in GitHub Desktop.
Save steveway/ae902966384de2bd4d2e81c5cc0af5ed to your computer and use it in GitHub Desktop.
A simple script that creates Greasepencil Layers and frames based on a .pg2 project. (Unfinished)
bl_info = {
"name": "Papagayo-NG 2D Importer",
"author": "Stefan Murawski",
"version": (0, 0, 1),
"blender": (2, 80, 0),
"location": "3D window > Tool Shelf",
"description": "Create GreasePencil Object with Layers and Keyframes from Papagayo-NG .pg2 files.",
"warning": "",
"wiki_url": ""
"Scripts/Import-Export/Papagayo 2D Importer",
"tracker_url": "",
"category": "Import-Export"}
import bpy
import json
import os
from bpy_extras.io_utils import ImportHelper
from bpy.types import Operator, PropertyGroup
from bpy.props import StringProperty, BoolProperty, EnumProperty, PointerProperty
class OT_TestOpenFilebrowser(Operator, ImportHelper):
bl_idname = "test.open_filebrowser"
bl_label = "Load Papagayo-NG Project"
bl_description = 'Load .pg2 or .json files. (Only .pg2 currently)'
filter_glob : StringProperty( default='*.pg2;*.json;', options={'HIDDEN'} )
def execute(self, context):
"""Do something with the selected file(s)."""
filename, extension = os.path.splitext(self.filepath)
scene = bpy.types.Scene
scene.pg_path = self.filepath
return {'FINISHED'}
class BTN_OP_create_grease_objects(Operator):
bl_idname = 'pg.create_objects'
bl_label = 'Start Processing'
bl_description = 'Create Grease Pencil Objects from Papagayo-NG files'
def execute(self, context):
scn = context.scene
obj = context.active_object
scene = bpy.types.Scene
create_grease_objects(scene.pg_path)
scene.pg_objects_created = True
return {'FINISHED'}
class BTN_OP_apply_to_timeline(Operator):
bl_idname = 'pg.apply_timeline'
bl_label = 'Start Processing'
bl_description = 'Create Grease Pencil Objects from Papagayo-NG files'
def execute(self, context):
scn = context.scene
obj = context.active_object
scene = bpy.types.Scene
fill_timeline(scene.pg_path)
return {'FINISHED'}
class MyProperties(PropertyGroup):
rest_frames: BoolProperty(
name="Enable Rest Frames",
description="If enabled inserts rest frames into empty frames after words and phrases.",
default = False
)
class PapagayoNGImporterUI(bpy.types.Panel):
bl_id_name = "pg_main_menu"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_label = "Papagayo-NG 2D Importer"
bl_category = 'Animation'
def draw(self, context):
obj = bpy.context.active_object
scn = bpy.context.scene
scene = bpy.types.Scene
layout = self.layout
col = layout.column()
mytool = context.scene.my_tool
col.prop(mytool, "rest_frames")
col.operator('test.open_filebrowser', text="Select Papagayo-NG Project File")
col.separator()
if scene.pg_path:
col.label(text=os.path.split(scene.pg_path)[1], icon="FILE_FOLDER")
else:
col.label(text="No File loaded", icon="FILE_FOLDER")
col.separator()
if scene.pg_path:
col.label(text="Used Phonemes:")
for phoneme in get_list_of_phonemes(scene.pg_path):
col.label(text=phoneme)
col.operator("pg.create_objects", text="Create Grease Pencil Objects")
if scene.pg_objects_created:
col.separator()
col.label(text="Add the Grease Pencil Objects to your scene.")
col.label(text="Simply click on the keyframes to edit them.", icon="KEYFRAME")
col.label(text="Add sound by choosing Add and the Speaker.")
col.label(text="Draw all Phonemes and press the next button.")
col.separator()
col.operator("pg.apply_timeline", text="Apply to Timeline")
def get_list_of_phonemes(file_path):
papagayo_file = open(file_path, "r")
papagayo_json = json.load(papagayo_file)
list_of_used_phonemes = []
if file_path.endswith(".pg2"):
for voice in papagayo_json["voices"]:
list_of_used_phonemes.append(voice["name"] + ":")
for phoneme_left, phoneme_right in zip(voice["used_phonemes"][::2], voice["used_phonemes"][1::2]):
list_of_used_phonemes.append("{} | {}".format(phoneme_left, phoneme_right))
else:
list_of_used_phonemes.append(papagayo_json["name"] + ":")
for phoneme_left, phoneme_right in zip(papagayo_json["used_phonemes"][::2], papagayo_json["used_phonemes"][1::2]):
list_of_used_phonemes.append("{} | {}".format(phoneme_left, phoneme_right))
papagayo_file.close()
return list_of_used_phonemes
def create_grease_objects(file_path):
papagayo_file = open(file_path, "r")
papagayo_json = json.load(papagayo_file)
FPS = papagayo_json["fps"]
bpy.context.scene.render.fps = FPS
scene = bpy.types.Scene
sound_path = ""
if os.path.isabs(papagayo_json["sound_path"]):
sound_path = papagayo_json["sound_path"]
else:
sound_path = os.path.join(os.path.dirname(file_path), papagayo_json["sound_path"])
if not bpy.data.sounds.items():
bpy.ops.sound.open_mono(filepath=sound_path)
scene.pg_sound_data = bpy.data.sounds[0]
prev_area_type = bpy.context.area.type
bpy.context.area.type = 'SEQUENCE_EDITOR'
bpy.ops.sequencer.sound_strip_add(filepath=sound_path, frame_start=0, channel=1)
bpy.context.area.type = prev_area_type
if file_path.endswith(".pg2"):
# Audio loads fine, can be used with manually added speaker, but this speaker stays silent...
"""
if not bpy.data.speakers.items():
bpy.ops.object.speaker_add()
speaker = bpy.data.speakers[0]
speaker.sound = scene.pg_sound_data
"""
NUM_FRAMES = papagayo_json["sound_duration"]
else:
NUM_FRAMES = papagayo_json["end_frame"]
FRAMES_SPACING = 1 # distance between frames
bpy.context.scene.frame_start = 0
bpy.context.scene.frame_end = NUM_FRAMES*FRAMES_SPACING
bpy.context.scene.frame_current = 0
if file_path.endswith(".pg2"):
for voice in papagayo_json["voices"]:
curr_name = voice["name"]
bpy.ops.object.mode_set(mode = 'OBJECT')
bpy.ops.object.gpencil_add(align="WORLD", location=(0,0,0),
scale=(1,1,1), type="EMPTY")
bpy.context.object.name = str(curr_name) + "_stroke"
if curr_name not in bpy.data.grease_pencils:
bpy.data.grease_pencils.new(curr_name)
for phoneme in voice["used_phonemes"]:
if phoneme not in bpy.data.grease_pencils[curr_name].layers:
bpy.data.grease_pencils[curr_name].layers.new(phoneme)
pho_layer = bpy.data.grease_pencils[curr_name].layers[phoneme]
try:
frame = pho_layer.frames[0]
except IndexError:
pho_layer.frames.new(0)
bpy.data.objects[curr_name + "_stroke"].data = bpy.data.grease_pencils[curr_name]
else:
curr_name = papagayo_json["name"]
bpy.ops.object.mode_set(mode = 'OBJECT')
bpy.ops.object.gpencil_add(align="WORLD", location=(0,0,0),
scale=(1,1,1), type="EMPTY")
bpy.context.object.name = str(curr_name) + "_stroke"
if curr_name not in bpy.data.grease_pencils:
bpy.data.grease_pencils.new(curr_name)
for phoneme in papagayo_json["used_phonemes"]:
if phoneme not in bpy.data.grease_pencils[curr_name].layers:
bpy.data.grease_pencils[curr_name].layers.new(phoneme)
pho_layer = bpy.data.grease_pencils[curr_name].layers[phoneme]
try:
frame = pho_layer.frames[0]
except IndexError:
pho_layer.frames.new(0)
bpy.data.objects[curr_name + "_stroke"].data = bpy.data.grease_pencils[curr_name]
papagayo_file.close()
def fill_timeline(file_path):
papagayo_file = open(file_path, "r")
papagayo_json = json.load(papagayo_file)
last_pos = 0
voice_list = []
if file_path.endswith(".pg2"):
for voice in papagayo_json["voices"]:
voice_list.append(voice)
else:
voice_list.append(papagayo_json)
for voice in voice_list:
curr_name = voice["name"]
if curr_name + "combined" in bpy.data.grease_pencils[curr_name].layers: # TODO: Show a warning and allow to abort
bpy.data.grease_pencils[curr_name].layers[curr_name + "combined"].clear()
else:
bpy.data.grease_pencils[curr_name].layers.new(curr_name + "combined")
for phrase in voice["phrases"]:
if bpy.context.scene.my_tool.rest_frames:
if phrase["start_frame"] > last_pos + 1:
base_frame = bpy.data.grease_pencils[curr_name].layers["rest"].frames[0]
new_frame = bpy.data.grease_pencils[curr_name].layers[curr_name + "combined"].frames.copy(base_frame)
new_frame.frame_number = last_pos + 1
for word in phrase["words"]:
if bpy.context.scene.my_tool.rest_frames:
if word["start_frame"] > last_pos + 1:
base_frame = bpy.data.grease_pencils[curr_name].layers["rest"].frames[0]
new_frame = bpy.data.grease_pencils[curr_name].layers[curr_name + "combined"].frames.copy(base_frame)
new_frame.frame_number = last_pos + 1
for phoneme in word["phonemes"]:
base_frame = bpy.data.grease_pencils[curr_name].layers[phoneme["text"]].frames[0]
new_frame = bpy.data.grease_pencils[curr_name].layers[curr_name + "combined"].frames.copy(base_frame)
new_frame.frame_number = phoneme["frame"]
last_pos = phoneme["frame"]
"""
if bpy.context.scene.my_tool.rest_frames:
for frame in range(bpy.context.scene.frame_start, bpy.context.scene.frame_end):
exists = False
try:
t_frame = bpy.data.grease_pencils[curr_name].layers[curr_name + "combined"].frames[frame]
exists = True
except IndexError:
exists = False
if exists:
base_frame = bpy.data.grease_pencils[curr_name].layers["rest"].frames[-1]
new_frame = bpy.data.grease_pencils[curr_name].layers[curr_name + "combined"].frames.copy(base_frame)
new_frame.frame_number = frame
"""
papagayo_file.close()
def create_keyframes(file_path):
papagayo_file = open(file_path, "r")
papagayo_json = json.load(papagayo_file)
FPS = papagayo_json["fps"]
bpy.context.scene.render.fps = FPS
if file_path.endswith(".pg2"):
NUM_FRAMES = papagayo_json["sound_duration"]
else: NUM_FRAMES = papagayo_json["end_frame"]
FRAMES_SPACING = 1 # distance between frames
bpy.context.scene.frame_start = 0
bpy.context.scene.frame_end = NUM_FRAMES*FRAMES_SPACING
voice_list = []
if file_path.endswith(".pg2"):
for voice in papagayo_json["voices"]:
voice_list.append(voice)
else:
voice_list.append(papagayo_json)
for voice in voice_list:
curr_name = voice["name"]
if curr_name not in bpy.data.grease_pencils:
bpy.data.grease_pencils.new(curr_name)
for phrase in voice["phrases"]:
for word in phrase["words"]:
for phoneme in word["phonemes"]:
if phoneme["text"] not in bpy.data.grease_pencils[curr_name].layers:
bpy.data.grease_pencils[curr_name].layers.new(phoneme["text"])
try:
pho_frame = bpy.data.grease_pencils[curr_name].layers[phoneme["text"]].frames.new(phoneme["frame"])
except RuntimeError:
pass
classes = (PapagayoNGImporterUI, BTN_OP_create_grease_objects, BTN_OP_apply_to_timeline, OT_TestOpenFilebrowser, MyProperties)
def register():
scene = bpy.types.Scene
scene.pg_path = ""
scene.pg_objects_created = False
for cls in classes:
bpy.utils.register_class(cls)
bpy.types.Scene.my_tool = PointerProperty(type=MyProperties)
def unregister():
for cls in classes:
bpy.utils.unregister_class(cls)
del bpy.types.Scene.my_tool
if __name__ == "__main__":
register()
@Hunanbean
Copy link

Thank you for this, and my apologies. The existence of this script went a bit unrecognized due to the usual workflow that had already been adopted.
The reference to the set FPS in Papagayo-NG seems to not be written to the JSON. Here is some relevant output:

line 46, in execute
    create_grease_objects(scene.pg_path)
line 109, in create_grease_objects
    FPS = papagayo_json["fps"]
KeyError: 'fps'

I may be misunderstanding the cause of the error, but there it is. Blender 2.93

Thank you

@steveway
Copy link
Author

Ah yes, the FPS information is not saved in the JSON export!
I updated Papagayo-NG to do that now.
For now that plugin only supports .pg2 project files.
But it should be possible to update it to support the exported JSON files too.

@steveway
Copy link
Author

steveway commented Sep 1, 2021

I updated this script to show the used phonemes.
And I tested this now in Blender 2.93, it actually works as expected.
The Frames are all combined into the combined layer and the playback shows the animation!
If you then add the sound file it will play that together with the animation.
I guess it was a bug of Blender itself before that my frames were empty.

@aziagiles
Copy link

wow. sounds awesome. Let me download and give it a try.

@steveway
Copy link
Author

steveway commented Sep 1, 2021

It should now be able to use the .json files from Papagayo-NG besides the .pg2 project files! (untested)

@aziagiles
Copy link

Wow. Looks great. Let me download and give it a try.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment