206 lines
7.2 KiB
Python
206 lines
7.2 KiB
Python
"""
|
||
knot_animation/__init__.py
|
||
--------------------------
|
||
Blender add-on entry-point for the AnimKnots procedural animation system.
|
||
|
||
What this add-on does
|
||
---------------------
|
||
1. Registers a VIEW_3D N-panel ("AnimKnots") with:
|
||
- A per-scene playlist of KnotItem entries, each specifying topology,
|
||
geometry, animation rates, transition settings, and a material preset.
|
||
- Operators for adding, removing, reordering, and randomly generating
|
||
playlist entries.
|
||
- Global playback controls (speed, phase, reactivity) that can be
|
||
keyframed or driven for audio-reactive animation.
|
||
|
||
2. On every frame change (frame_change_post handler), procedurally rebuilds
|
||
a single reused NURBS curve object ("AnimKnot") in-place using the
|
||
parametric torus-knot equations — no bpy.ops, no edit-mode, no depsgraph
|
||
races.
|
||
|
||
3. Interpolates geometry and cross-fades shader materials between adjacent
|
||
playlist entries during configurable transition windows.
|
||
|
||
4. Provides a catalogue of 20 built-in shader presets (GLOSS_BLUE, NEON_GLOW,
|
||
METALLIC, GLASS, HOLOGRAM, LAVA, IRIDESCENT, …) plus support for arbitrary
|
||
project materials.
|
||
|
||
Module layout
|
||
-------------
|
||
types.py — KnotConfig TypedDict (shared data contract)
|
||
constants.py — pure-data defaults (KNOT_CONFIGS, camera/light positions, …)
|
||
compat.py — one-shot compatibility patches (headless align_matrix fix)
|
||
materials.py — 20 shader builders + material cache + blend system
|
||
geometry.py — _make_torus_knot() (parametric NURBS, no bpy.context reads)
|
||
handler.py — @persistent frame-change handler + easing + playlist timing
|
||
properties.py — Blender PropertyGroups (KnotItem, KnotGlobalSettings, …)
|
||
operators.py — KNOT_OT_* operators
|
||
ui.py — KNOT_UL_* lists, draw_knot_properties(), KNOT_PT_Panel
|
||
scene_setup.py — setup_scene() (camera, light, world, render settings)
|
||
|
||
How to use
|
||
----------
|
||
Option A – Blender Text Editor:
|
||
Open knot_animation/__init__.py, click Run Script (Alt+P).
|
||
|
||
Option B – Command line:
|
||
blender.exe --python knot_animation/__init__.py
|
||
|
||
Option C – Persistent add-on:
|
||
Copy the knot_animation/ folder to Blender's addons directory and enable
|
||
it from Edit → Preferences → Add-ons. The panel appears under the
|
||
"AnimKnots" tab in the 3-D viewport N-panel.
|
||
"""
|
||
|
||
if "bpy" in locals():
|
||
import importlib
|
||
importlib.reload(compat)
|
||
importlib.reload(constants)
|
||
importlib.reload(types)
|
||
importlib.reload(materials)
|
||
importlib.reload(geometry)
|
||
importlib.reload(handler)
|
||
importlib.reload(properties)
|
||
importlib.reload(operators)
|
||
importlib.reload(ui)
|
||
importlib.reload(scene_setup)
|
||
else:
|
||
import bpy
|
||
from . import compat, constants, types, materials, geometry, handler, properties, operators, ui, scene_setup
|
||
|
||
from .compat import apply_compat_patches
|
||
from .properties import (
|
||
KnotAllowedMaterial,
|
||
KnotGlobalSettings,
|
||
KnotItem,
|
||
KnotGeneratorSettings,
|
||
)
|
||
from .operators import (
|
||
KNOT_OT_Add,
|
||
KNOT_OT_Remove,
|
||
KNOT_OT_Move,
|
||
KNOT_OT_Populate,
|
||
KNOT_OT_BakePreview,
|
||
KNOT_OT_SyncGeneratorMaterials,
|
||
KNOT_OT_FitTimeline,
|
||
KNOT_OT_FitPlaylist,
|
||
KNOT_OT_GenerateRandom,
|
||
)
|
||
from .ui import (
|
||
KNOT_UL_List,
|
||
KNOT_UL_AllowedMaterialsList,
|
||
KNOT_PT_Panel,
|
||
)
|
||
from .geometry import _remove_existing_knot
|
||
from .handler import knot_frame_handler
|
||
from .scene_setup import setup_scene
|
||
from .constants import KNOT_MAT_NAME, SHADER_BLEND_MAT_NAME
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Add-on metadata
|
||
# ---------------------------------------------------------------------------
|
||
|
||
bl_info = {
|
||
"name": "AnimKnots",
|
||
"author": "knot_animation project",
|
||
"version": (2, 0, 0),
|
||
"blender": (4, 0, 0),
|
||
"location": "View3D > Sidebar > AnimKnots",
|
||
"description": "Procedural torus-knot animation with playlist, transitions, and 20 shader presets",
|
||
"category": "Animation",
|
||
}
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Registered Blender classes (order matters for PropertyGroup dependencies)
|
||
# ---------------------------------------------------------------------------
|
||
|
||
classes = (
|
||
KnotAllowedMaterial,
|
||
KnotGlobalSettings,
|
||
KnotItem,
|
||
KnotGeneratorSettings,
|
||
KNOT_UL_List,
|
||
KNOT_UL_AllowedMaterialsList,
|
||
KNOT_OT_SyncGeneratorMaterials,
|
||
KNOT_OT_Add,
|
||
KNOT_OT_Remove,
|
||
KNOT_OT_Move,
|
||
KNOT_OT_Populate,
|
||
KNOT_OT_BakePreview,
|
||
KNOT_OT_FitTimeline,
|
||
KNOT_OT_FitPlaylist,
|
||
KNOT_OT_GenerateRandom,
|
||
KNOT_PT_Panel,
|
||
)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Lifecycle
|
||
# ---------------------------------------------------------------------------
|
||
|
||
def register() -> None:
|
||
apply_compat_patches()
|
||
|
||
for cls in classes:
|
||
bpy.utils.register_class(cls)
|
||
|
||
bpy.types.Scene.knot_list = bpy.props.CollectionProperty(type=KnotItem)
|
||
bpy.types.Scene.knot_list_index = bpy.props.IntProperty(name="Index", default=0)
|
||
bpy.types.Scene.knot_generator = bpy.props.PointerProperty(type=KnotGeneratorSettings)
|
||
bpy.types.Scene.knot_globals = bpy.props.PointerProperty(type=KnotGlobalSettings)
|
||
|
||
# Register to frame_change_post so the handler fires *after* Blender has
|
||
# evaluated the new frame, preventing the freshly-built mesh from being
|
||
# overwritten by the depsgraph update.
|
||
handlers = bpy.app.handlers.frame_change_post
|
||
for h in list(handlers):
|
||
if getattr(h, "__name__", "") == knot_frame_handler.__name__:
|
||
handlers.remove(h)
|
||
handlers.append(knot_frame_handler)
|
||
|
||
print("[KnotScript] Handler and UI registered.")
|
||
|
||
|
||
def unregister() -> None:
|
||
for cls in reversed(classes):
|
||
bpy.utils.unregister_class(cls)
|
||
|
||
del bpy.types.Scene.knot_list
|
||
del bpy.types.Scene.knot_list_index
|
||
del bpy.types.Scene.knot_generator
|
||
del bpy.types.Scene.knot_globals
|
||
|
||
handlers = bpy.app.handlers.frame_change_post
|
||
for h in list(handlers):
|
||
if getattr(h, "__name__", "") == knot_frame_handler.__name__:
|
||
handlers.remove(h)
|
||
|
||
_remove_existing_knot()
|
||
|
||
# Purge all cached shader and blend materials on unregister to avoid
|
||
# data-block buildup on script reload. Only remove zero-user materials.
|
||
for mat in list(bpy.data.materials):
|
||
if (mat.name.startswith("KnotShader_")
|
||
or mat.name.startswith("KnotBlend_")
|
||
or mat.name.startswith("KnotItem_Preset_")
|
||
or mat.name in (KNOT_MAT_NAME, SHADER_BLEND_MAT_NAME)):
|
||
if mat.users == 0:
|
||
bpy.data.materials.remove(mat)
|
||
|
||
print("[KnotScript] Handler and UI unregistered.")
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Entry point (run as script or via --python flag)
|
||
# ---------------------------------------------------------------------------
|
||
|
||
if __name__ == "__main__":
|
||
register()
|
||
setup_scene()
|
||
|
||
# Generate the knot for the current frame immediately so the viewport is
|
||
# not empty when the script first runs.
|
||
knot_frame_handler(bpy.context.scene)
|
||
print("[KnotScript] Ready. Start scrubbing the timeline!")
|
||
print(" Press Space to play, or render with Ctrl+F12.")
|