2025-12-01

This commit is contained in:
2026-03-17 14:58:51 -06:00
parent 183e865f8b
commit 4b82b57113
6846 changed files with 954887 additions and 162606 deletions
@@ -0,0 +1,168 @@
import bpy # type: ignore
class BakeParticlesOperator(bpy.types.Operator):
"""Bake all Particles to Keyframes. The particles need to have an instance object set. The baked instances are moved to a new collection."""
bl_idname = "object.bake_particles"
bl_label = "Bake Particles"
KEYFRAME_LOCATION: bpy.props.BoolProperty()
KEYFRAME_ROTATION: bpy.props.BoolProperty()
KEYFRAME_SCALE: bpy.props.BoolProperty()
# Viewport and render visibility.
KEYFRAME_VISIBILITY: bpy.props.BoolProperty()
@classmethod
def poll(cls, context):
obj = context.object
if not obj:
return False
if not hasattr(obj, "particle_systems"):
return False
if not len(obj.particle_systems) > 0:
return False
for particlesys in obj.particle_systems:
if bpy.data.particles[particlesys.settings.name].instance_object is None:
return False
return True
def create_particle_collection(self, collection_name):
# Create or clear the particle collection.
# Create a new collection and link it to the scene.
collection_name = bpy.context.scene.particle_settings.collection_name
particle_collection = bpy.data.collections.get(collection_name)
if particle_collection is None:
particle_collection = bpy.data.collections.new(collection_name)
if bpy.context.scene.collection.children.get(collection_name) is None:
bpy.context.scene.collection.children.link(particle_collection)
# remove all objects from the particle collection
if len(particle_collection.objects) > 0:
rem_obj_names = []
for obj in particle_collection.objects:
rem_obj_names.append(obj.name)
for rem_name in rem_obj_names:
bpy.data.objects.remove(bpy.data.objects[rem_name])
def create_objects_for_particles(self, ps, obj, collection_name):
# Duplicate the given object for every particle and return the duplicates.
# Use instances instead of full copies.
obj_list = []
mesh = obj.data
particle_collection = bpy.data.collections.get(collection_name)
for particle in ps.particles:
dupli = bpy.data.objects.new(name=obj.name, object_data=mesh)
particle_collection.objects.link(dupli)
obj_list.append(dupli)
# copy modifiers to duplicates
# adapted from: https://blender.stackexchange.com/a/4883
for modifierOrig in obj.modifiers:
modifierNew = dupli.modifiers.new(modifierOrig.name, modifierOrig.type)
# collect names of writable properties
properties = [
p.identifier
for p in modifierOrig.bl_rna.properties
if not p.is_readonly
]
# copy those properties
for prop in properties:
setattr(modifierNew, prop, getattr(modifierOrig, prop))
return obj_list
def match_and_keyframe_objects(self, ps, obj_list, start_frame, end_frame):
# Match and keyframe the objects to the particles for every frame in the
# given range.
frame_offset = bpy.context.scene.particle_settings.frame_offset
for frame in range(start_frame, end_frame + 1, frame_offset):
bpy.context.scene.frame_set(frame)
for p, obj in zip(ps.particles, obj_list):
self.match_object_to_particle(p, obj)
self.keyframe_obj(obj)
def match_object_to_particle(self, p, obj):
# Match the location, rotation, scale and visibility of the object to
# the particle.
loc = p.location
rot = p.rotation
size = p.size
# Set rotation mode to quaternion to match particle rotation.
obj.rotation_mode = "QUATERNION"
obj.rotation_quaternion = rot
if self.KEYFRAME_VISIBILITY:
if p.alive_state != "ALIVE":
size *= 0.01
obj.location = loc
obj.scale = (size, size, size)
# obj.hide_viewport = not(vis)
# obj.hide_render = not(vis)
def keyframe_obj(self, obj):
# Keyframe location, rotation, scale and visibility if specified.
if self.KEYFRAME_LOCATION:
obj.keyframe_insert("location")
if self.KEYFRAME_ROTATION:
obj.keyframe_insert("rotation_quaternion")
if self.KEYFRAME_SCALE:
obj.keyframe_insert("scale")
# if self.KEYFRAME_VISIBILITY:
# obj.keyframe_insert("hide_viewport")
# obj.keyframe_insert("hide_render")
def execute(self, context):
# go to start frame
bpy.context.scene.frame_set(0)
collection_name = bpy.context.scene.particle_settings.collection_name
self.create_particle_collection(collection_name)
# get emitter and instance
emitter = bpy.context.object
ps_list = emitter.particle_systems
for i, ps in enumerate(ps_list):
instance = ps.settings.instance_object
depsgraph = bpy.context.evaluated_depsgraph_get()
# Extract locations
ps = depsgraph.objects[emitter.name].particle_systems[i]
# update ps hack
# bpy.data.particles[ps.name].count += 1
# bpy.data.particles[ps.name].count -= 1
start_frame = bpy.context.scene.frame_start
end_frame = bpy.context.scene.frame_end
obj_list = self.create_objects_for_particles(ps, instance, collection_name)
self.match_and_keyframe_objects(ps, obj_list, start_frame, end_frame)
# Simplify
bpy.ops.object.select_all(action="DESELECT")
for obj in bpy.data.collections[collection_name].all_objects:
obj.select_set(True)
return {"FINISHED"}
def register():
bpy.utils.register_class(BakeParticlesOperator)
def unregister():
bpy.utils.unregister_class(BakeParticlesOperator)
@@ -0,0 +1,122 @@
import bpy # type: ignore
class JoinAnimationOperator(bpy.types.Operator):
bl_idname = "scene.join_anim"
bl_label = "Join Animation"
bl_description = "Join animations for all selected objects by making an NLA strip for each object and naming the track consistently"
bl_options = {"REGISTER"}
anim_name: bpy.props.StringProperty()
@classmethod
def poll(cls, context):
return True
def execute(self, context):
sel_objects = context.selected_objects
for obj in sel_objects:
if obj.animation_data is None:
continue
if obj.animation_data.action is None:
continue
# if hasattr(obj.animation_data,"action"):
action = obj.animation_data.action
track = obj.animation_data.nla_tracks.new()
track.strips.new(self.anim_name, int(action.frame_start), action)
track.name = self.anim_name
obj.animation_data.action = None
return {"FINISHED"}
class SeperateAnimationOperator(bpy.types.Operator):
bl_idname = "scene.seperate_anim"
bl_label = "Separate Animation"
bl_description = "Transform NLA strips back to Keyframes to make them editable again. Make sure to select all objects you want to transform back."
bl_options = {"REGISTER"}
@classmethod
def poll(cls, context):
return True
def execute(self, context):
sel_objects = context.selected_objects
for obj in sel_objects:
if obj.animation_data is None:
continue
if len(obj.animation_data.nla_tracks) == 0:
continue
# set actions
track = obj.animation_data.nla_tracks[0]
action_name = track.strips[0].name
action = bpy.data.actions.get(action_name)
obj.animation_data.action = action
# remove track
obj.animation_data.nla_tracks.remove(track)
return {"FINISHED"}
class RenameNLAAnimationOperator(bpy.types.Operator):
bl_idname = "scene.rename_anim"
bl_label = "Rename Animation"
bl_description = "Rename NLA Tracks on selected objects"
bl_options = {"REGISTER"}
anim_name: bpy.props.StringProperty()
index = 0
@classmethod
def poll(cls, context):
return True
def execute(self, context):
sel_objects = context.selected_objects
for obj in sel_objects:
track = obj.animation_data.nla_tracks[self.index]
track.name = self.anim_name
return {"FINISHED"}
class RenameActionOperator(bpy.types.Operator):
bl_idname = "scene.rename_action"
bl_label = "Rename Action"
bl_description = "Rename Action on active object"
bl_options = {"REGISTER"}
action_name: bpy.props.StringProperty()
index = 0
@classmethod
def poll(cls, context):
return True
def execute(self, context):
active_object = context.active_object
active_object.animation_data.action.name = self.action_name
return {"FINISHED"}
def register():
# Use actions instead of NLA Strips for Animation merging
if bpy.app.version < (4, 4, 0):
bpy.utils.register_class(JoinAnimationOperator)
bpy.utils.register_class(SeperateAnimationOperator)
bpy.utils.register_class(RenameNLAAnimationOperator)
else:
bpy.utils.register_class(RenameActionOperator)
def unregister():
# Use actions instead of NLA Strips for Animation merging
if bpy.app.version < (4, 4, 0):
bpy.utils.unregister_class(JoinAnimationOperator)
bpy.utils.unregister_class(SeperateAnimationOperator)
bpy.utils.unregister_class(RenameNLAAnimationOperator)
else:
bpy.utils.unregister_class(RenameActionOperator)
@@ -0,0 +1,361 @@
import bpy # type: ignore
from pathlib import Path
from ..Functions import functions
class GOVIE_open_export_folder_Operator(bpy.types.Operator):
bl_idname = "scene.open_export_folder"
bl_label = "Open Folder"
bl_description = "Open current GLB folder. You may need to export first for the folder to be created."
@classmethod
def poll(cls, context):
# get folder of blend file
blend_path = Path(bpy.data.filepath).parent
# get export settings
glb_filename = context.scene.export_settings.glb_filename
if blend_path.joinpath(glb_filename).parent.exists():
return True
else:
return False
def execute(self, context):
# get folder of blend file
blend_path = Path(bpy.data.filepath).parent
# get export settings
glb_filename = context.scene.export_settings.glb_filename
bpy.ops.wm.url_open(
url="file://{}".format(
str(blend_path.joinpath(glb_filename).parent.absolute())
)
)
return {"FINISHED"}
class GOVIE_Open_Link_Operator(bpy.types.Operator):
bl_idname = "scene.open_link"
bl_label = "Open Website"
bl_description = "Go to GOVIE Website"
url: bpy.props.StringProperty(name="url")
@classmethod
def poll(cls, context):
return True
def execute(self, context):
bpy.ops.wm.url_open(url=self.url)
return {"FINISHED"}
class GOVIE_Add_Property_Operator(bpy.types.Operator):
"""Add the custom property on the current selected object"""
bl_idname = "object.add_property"
bl_label = "Add custom Property"
property_type: bpy.props.StringProperty(name="custom_property_name")
@classmethod
def poll(cls, context):
if context.object is None:
return False
else:
return True
def execute(self, context):
selected_objects = context.selected_objects
for obj in selected_objects:
if self.property_type == "visibility":
obj["visibility"] = 1
# obj.visibility_bool = 1
if self.property_type == "clickable":
obj["clickablePart"] = "clickablePart"
return {"FINISHED"}
class GOVIE_Remove_Property_Operator(bpy.types.Operator):
"""Remove the custom property on the current selected object"""
bl_idname = "object.remove_property"
bl_label = "Remove visibility Property"
property_type: bpy.props.StringProperty(name="custom_property_name")
@classmethod
def poll(cls, context):
if context.object is None:
return False
else:
return True
def execute(self, context):
selected_objects = context.selected_objects
for obj in selected_objects:
if self.property_type == "visibility":
if "visibility" in obj.keys():
del obj["visibility"]
if self.property_type == "clickable":
if "clickablePart" in obj.keys():
del obj["clickablePart"]
return {"FINISHED"}
class GOVIE_Quick_Export_GLB_Operator(bpy.types.Operator):
bl_idname = "scene.gltf_quick_export"
bl_label = "EXPORT_GLTF"
bl_description = "Save Blend file first ! The GLB file will be saved to 'pathOfBlendFile/glb/filename.glb'"
@classmethod
def poll(cls, context):
if bpy.data.is_saved:
return True
else:
return False
def execute(self, context):
# check spelling
filename = context.scene.export_settings.glb_filename
context.scene.export_settings.glb_filename = functions.convert_umlaut(filename)
# check annotation names
functions.rename_annotation()
# blender file saved
file_is_saved = bpy.data.is_saved
if not file_is_saved:
self.report({"INFO"}, "You need to save the Blend file first!")
return {"FINISHED"}
# get folder of blend file
blend_path = Path(bpy.data.filepath).parent
# get export settings
glb_filename = context.scene.export_settings.glb_filename
glb_filepath = blend_path.joinpath(glb_filename)
if not glb_filepath.parent.exists():
glb_filepath.parent.mkdir(parents=True)
preset_path = "operator/export_scene.gltf/"
export_preset_name = context.scene.export_settings.export_preset
preset_filepath = bpy.utils.preset_find(export_preset_name, preset_path)
gltf_export_param = {}
# read preset parameters from file
if preset_filepath:
class Container(object):
__slots__ = ("__dict__",)
op = Container()
preset_file = open(preset_filepath, "r")
# storing the values from the preset on the class
for line in preset_file.readlines()[3::]:
exec(line, globals(), locals())
# pass class dictionary to the operator
gltf_export_param = op.__dict__
else:
gltf_export_param["export_extras"] = True
join_objects = context.scene.export_settings.join_objects
gltf_export_param["filepath"] = str(glb_filepath.absolute())
# export glb
if join_objects:
functions.optimize_scene(gltf_export_param)
else:
bpy.ops.export_scene.gltf(**gltf_export_param)
# change glb dropdown entry
# context.scene.glb_file_dropdown = context.scene.export_settings.glb_filename
return {"FINISHED"}
class GOVIE_CleanupMesh_Operator(bpy.types.Operator):
bl_idname = "object.cleanup_mesh"
bl_label = "Delete Loose and Degenerate Dissolve"
bl_description = "Mesh Cleanup -> Delete Loose and Degenerate Dissolve"
@classmethod
def poll(cls, context):
return context.mode == "OBJECT"
def execute(self, context):
exclude_temp_list = []
collections = bpy.context.view_layer.layer_collection.children
# switch on all layers but remember vis settings
for collection in collections:
exclude_temp_list.append(collection.exclude)
collection.exclude = False
for obj in context.scene.objects:
if obj.type == "MESH":
functions.select_object(self, obj)
bpy.ops.object.editmode_toggle()
bpy.ops.mesh.delete_loose()
bpy.ops.mesh.dissolve_degenerate()
bpy.ops.object.editmode_toggle()
# set back layer settings
for collection, exclude_temp_value in zip(collections, exclude_temp_list):
collection.exclude = exclude_temp_value
self.report({"INFO"}, "Meshes Cleaned !")
return {"FINISHED"}
class GOVIE_CheckTexNodes_Operator(bpy.types.Operator):
"""Check if there are any empty Texture Nodes in any Material and print that material"""
bl_idname = "object.check_tex_nodes"
bl_label = "Check Empty Tex Nodes"
bpy.types.Scene.mat_name_list = []
def execute(self, context):
mat_name_list = context.scene.mat_name_list
mat_name_list.clear()
# get materials with texture nodes that have no image assigned
for mat in bpy.data.materials:
if mat.node_tree is None:
continue
for node in mat.node_tree.nodes:
if node.type == "TEX_IMAGE" and node.image is None:
mat_name_list.append(mat.name)
self.report(
{"INFO"},
"Found empty image node in material {}".format(mat.name),
)
functions.select_object_by_mat(self, mat)
if len(mat_name_list) == 0:
self.report({"INFO"}, "No Empty Image Nodes")
return {"FINISHED"}
class GOVIE_Add_UV_Animation_Operator(bpy.types.Operator):
"""Create UV Animation for selected object"""
bl_idname = "object.add_uv_anim"
bl_label = "Add UV Animation"
@classmethod
def poll(cls, context):
if context.object is None:
return False
else:
return True
def execute(self, context):
active_object = context.active_object
new_name = active_object.name + "_uv_anim_controller"
if bpy.data.objects.get(new_name):
empty = bpy.data.objects[new_name]
else:
bpy.ops.object.empty_add(
type="PLAIN_AXES", align="WORLD", location=(0, 0, 0), scale=(1, 1, 1)
)
empty = context.active_object
empty.name = new_name
# add custom property
empty["uvAnim"] = active_object.name
# save emtpy name in scene for later use
context.scene["uv_anim_obj"] = empty.name
# add driver to material mapping node
if active_object and active_object.active_material:
material = active_object.active_material
# Find the Mapping node in the material's node tree
mapping_node = None
for node in material.node_tree.nodes:
print(node.type)
if node.type == "MAPPING":
mapping_node = node
break
if mapping_node:
# remove driver fist if there is one
mapping_node.inputs["Location"].driver_remove("default_value", 0)
driverX = (
mapping_node.inputs["Location"]
.driver_add("default_value", 0)
.driver
)
driverX.type = "SCRIPTED"
driverX.expression = empty.name
# Add the Empty object as a variable target
var = driverX.variables.new()
var.name = empty.name
var.type = "TRANSFORMS"
var.targets[0].id = bpy.data.objects[empty.name]
var.targets[0].transform_type = "LOC_X"
mapping_node.inputs["Location"].driver_remove("default_value", 1)
driverY = (
mapping_node.inputs["Location"]
.driver_add("default_value", 1)
.driver
)
driverY.type = "SCRIPTED"
driverY.expression = "-" + empty.name
var = driverY.variables.new()
var.name = empty.name
var.type = "TRANSFORMS"
var.targets[0].id = bpy.data.objects[empty.name]
var.targets[0].transform_type = "LOC_Z"
else:
print("Mapping node not found in the material's node tree.")
else:
print("Active object or active material not found.")
active_object.select_set(True)
context.view_layer.objects.active = active_object
return {"FINISHED"}
def register():
bpy.utils.register_class(GOVIE_open_export_folder_Operator)
bpy.utils.register_class(GOVIE_Open_Link_Operator)
bpy.utils.register_class(GOVIE_Add_Property_Operator)
bpy.utils.register_class(GOVIE_Remove_Property_Operator)
bpy.utils.register_class(GOVIE_Quick_Export_GLB_Operator)
bpy.utils.register_class(GOVIE_CleanupMesh_Operator)
bpy.utils.register_class(GOVIE_CheckTexNodes_Operator)
bpy.utils.register_class(GOVIE_Add_UV_Animation_Operator)
def unregister():
bpy.utils.unregister_class(GOVIE_open_export_folder_Operator)
bpy.utils.unregister_class(GOVIE_Open_Link_Operator)
bpy.utils.unregister_class(GOVIE_Add_Property_Operator)
bpy.utils.unregister_class(GOVIE_Remove_Property_Operator)
bpy.utils.unregister_class(GOVIE_Quick_Export_GLB_Operator)
bpy.utils.unregister_class(GOVIE_CleanupMesh_Operator)
bpy.utils.unregister_class(GOVIE_CheckTexNodes_Operator)
bpy.utils.unregister_class(GOVIE_Add_UV_Animation_Operator)
@@ -0,0 +1,53 @@
from pathlib import Path
import bpy # type: ignore
from ..Functions import functions
class GOVIE_Preview_Operator(bpy.types.Operator):
bl_idname = "scene.open_web_preview"
bl_label = "Open in Browser"
bl_description = "Press export to display a preview of the exported file"
port = 8000
url = (
"https://3dit-tools.s3.eu-central-1.amazonaws.com/StaticGLBViewerV2/index.html"
)
@classmethod
def poll(cls, context):
# get folder of blend file
blend_path = Path(bpy.data.filepath).parent
# get export settings
glb_filename = context.scene.export_settings.glb_filename
if not glb_filename.endswith(".glb"):
glb_filename = "{}.glb".format(glb_filename)
if blend_path.joinpath(glb_filename).exists():
return True
else:
return False
def execute(self, context):
# get folder of blend file
blend_path = Path(bpy.data.filepath).parent
# get export settings
glb_filename = context.scene.export_settings.glb_filename
if not glb_filename.endswith(".glb"):
glb_filename = "{}.glb".format(glb_filename)
functions.start_server(blend_path.joinpath(glb_filename).absolute(), self.port)
# run browser
bpy.ops.wm.url_open(url=self.url)
return {"FINISHED"}
def register():
bpy.utils.register_class(GOVIE_Preview_Operator)
def unregister():
bpy.utils.unregister_class(GOVIE_Preview_Operator)
@@ -0,0 +1,43 @@
import bpy # type: ignore
class SimplifyKeyframes(bpy.types.Operator):
bl_idname = "scene.simplify_keyframes"
bl_label = "Simplify Keyframes"
bl_description = "Simplify the Keyframes by the selected decimate ratio, higher values will reduce more keyframes"
bl_options = {"REGISTER"}
decimate_ratio: bpy.props.FloatProperty()
mode: bpy.props.StringProperty()
@classmethod
def poll(cls, context):
display_button = False
# at least one object with animation on it
sel_objects = context.selected_objects
for obj in sel_objects:
if obj.animation_data is not None:
display_button = True
return display_button
def execute(self, context):
context.area.type = "GRAPH_EDITOR"
bpy.ops.graph.select_all(action="SELECT")
bpy.ops.graph.decimate(
mode=self.mode,
factor=self.decimate_ratio,
remove_error_margin=self.decimate_ratio,
)
context.area.type = "VIEW_3D"
return {"FINISHED"}
def register():
bpy.utils.register_class(SimplifyKeyframes)
def unregister():
bpy.utils.unregister_class(SimplifyKeyframes)