2 Commits

Author SHA1 Message Date
Stefan Cepko 8292682ac2 Add example scripts for major functions 2026-06-05 05:36:50 -04:00
Stefan Cepko 31e464f5cd Release 0.1 2026-06-05 04:47:34 -04:00
15 changed files with 1052 additions and 56 deletions
+31
View File
@@ -0,0 +1,31 @@
name: Build & Release Addon (Gitea)
on:
push:
tags:
- 'v*'
workflow_dispatch:
jobs:
build-and-release:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Build Zip Package
run: python package_addon.py
- name: Create Release
uses: https://gitea.com/actions/release-action@main
with:
files: |
releases/*.zip
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+36
View File
@@ -0,0 +1,36 @@
name: Build & Release Addon (GitHub)
on:
push:
tags:
- 'v*'
workflow_dispatch:
permissions:
contents: write
jobs:
build-and-release:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Build Zip Package
run: python package_addon.py
- name: Create Release and Upload Asset
uses: softprops/action-gh-release@v2
with:
files: |
releases/*.zip
draft: false
prerelease: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+127 -42
View File
@@ -8,10 +8,10 @@ A Blender add-on that generates a continuously morphing parade of procedural [to
## Features
- **20 built-in shader presets** — Gloss Blue, Neon Glow, Metallic, Glass, Hologram, Lava, Iridescent, Matte Clay, Ghost, Car Paint, Chrome, Ruby Glass, Frosted Ice, Lichen Rough, Copper Patina, Zebra Stripes, Wood Veneer, Carbon Fiber, Pearlescent, Plasma Glow
- **Playlist-based animation** — add, remove, reorder any number of knots; each entry controls its own topology, geometry, material, and animation rates
- **Smooth transitions** — configurable per-knot morph windows with Linear, Quadratic In/Out, or Smoothstep easing
- **Audio-reactive hooks** — drive the global Reactivity factor from a sound driver to pulse spin, height, and scale rates in sync with audio
- **20 built-in shader presets** — Gloss Blue, Neon Glow, Metallic, Glass, Hologram, Lava, Iridescent, Matte Clay, Ghost, Car Paint, Chrome, Ruby Glass, Frosted Ice, Lichen Rough, Copper Patina, Zebra Stripes, Wood Veneer, Carbon Fiber, Pearlescent, Plasma Glow
- **Random playlist generator** — configure min/max ranges for every parameter and generate an entire playlist in one click
- **Bake & Export** — bake the procedural animation to a standalone `.blend` with per-frame mesh visibility keyframes; no addon required at render time; supports split-file export for SheepIt / distributed render farms
- **Blender 5.0+ native** — uses the layered Action API (`action → layers → strips → channelbag → fcurves`) and Blender 5 Principled BSDF input names directly
@@ -24,37 +24,74 @@ A Blender add-on that generates a continuously morphing parade of procedural [to
|---|---|
| Blender | **5.0** or later |
| Blender built-in addon | **Add Curve: Extra Objects** (auto-enabled by the script) |
> **Note:** The single-file version (`knot_animation.py`) has no installation step — just open and run in the Text Editor.
---
## Installation
## Installation & Running
### Option A — Persistent Add-on (recommended)
### Option A — Persistent Add-on (Recommended for Users)
1. Download or clone this repository.
2. In Blender, go to **Edit → Preferences → Add-ons → Install…**
3. Zip the `knot_animation/` folder and select the zip (Blender 5 requires a zip for local installs).
4. Enable **"Pr3tz"** from the add-on list.
5. Open the **3D Viewport → N-panel → Pr3tz** tab.
1. Download a release zip (or run `package_addon.py` to build one).
2. In Blender, go to **Edit → Preferences → Get Extensions → Add-ons** (or **Install from Disk...** from the top right gear menu).
3. Select the zip file (e.g. `releases/pr3tz_v2.0.0.zip`).
4. Enable **"Pr3tz"** from the list.
5. Access the controls in the **3D Viewport → N-panel (sidebar) → Pr3tz** tab.
### Option B — Run as a Script (no installation)
### Option B — Symlink (Recommended for Developers)
```
# From inside Blender's Text Editor:
# File → Open → knot_animation/__init__.py → Run Script (Alt+P)
To test and develop with live code updates without having to reinstall the addon:
1. Open your terminal as Administrator (on Windows) or with standard user permissions (on Linux/macOS).
2. Run the appropriate command for your OS/shell to link the `pr3tz/` folder directly to Blender's local addons directory (replace path with your Blender version/path if needed):
# Or from the command line:
"C:\Program Files\Blender Foundation\Blender 5.0\blender.exe" --python knot_animation/__init__.py
**PowerShell (Windows):**
```powershell
New-Item -ItemType SymbolicLink -Path "$env:APPDATA\Blender Foundation\Blender\5.0\scripts\addons\pr3tz" -Value "$PWD\pr3tz"
```
### Option C — Single-file version
`knot_animation_single_file.py` is a self-contained copy of the entire add-on in one file, useful for quick testing without touching Blender's add-on directory.
**CMD (Windows):**
```cmd
mklink /d "%APPDATA%\Blender Foundation\Blender\5.0\scripts\addons\pr3tz" "%CD%\pr3tz"
```
blender.exe --python knot_animation_single_file.py
**Bash (Linux):**
```bash
ln -s "$PWD/pr3tz" "$HOME/.config/blender/5.0/scripts/addons/pr3tz"
```
**Bash (macOS):**
```bash
ln -s "$PWD/pr3tz" "$HOME/Library/Application Support/Blender/5.0/scripts/addons/pr3tz"
```
3. Enable **"Pr3tz"** in Blender's Preferences.
4. Press `F3` and search for **"Reload Scripts"** to reload changes instantly.
### Option C — Bootstrap Loader (No Installation)
Since Python relative imports fail when running individual module files directly, you can run the bootstrap script from the repository root:
1. Open Blender.
2. In the **Text Editor**, open [run_script.py](run_script.py).
3. Click **Run Script** (or press `Alt+P`).
Alternatively, load it directly from the terminal:
**PowerShell (Windows):**
```powershell
& "C:\Program Files\Blender Foundation\Blender 5.0\blender.exe" --python run_script.py
```
**CMD (Windows):**
```cmd
"C:\Program Files\Blender Foundation\Blender 5.0\blender.exe" --python run_script.py
```
**Bash (Linux):**
```bash
blender --python run_script.py
```
**Bash (macOS):**
```bash
/Applications/Blender.app/Contents/MacOS/Blender --python run_script.py
```
---
@@ -69,6 +106,33 @@ blender.exe --python knot_animation_single_file.py
---
## Programmatic Examples
The `examples/` directory contains 8 different Python scripts demonstrating how to interact with the Pr3tz animation engine programmatically:
1. [01_simple_torus_knot.py](examples/01_simple_torus_knot.py) — Demonstrates basic torus knot generation with customizable coprime topology parameters.
2. [02_multi_link_knot.py](examples/02_multi_link_knot.py) — Shows how to generate multi-link torus knots when $p$ and $q$ are not coprime, producing multiple separate interlocking curve loops.
3. [03_shape_types.py](examples/03_shape_types.py) — Shows generating and editing other shapes supported by the engine (Mobius Strip, 3D Lissajous, Spherical Spiral).
4. [04_animation_styles.py](examples/04_animation_styles.py) — Exercises the four different built-in animation modes (spin, revolution, vertical height breathing, and scale pulsation).
5. [05_custom_transitions.py](examples/05_custom_transitions.py) — Configures morph transitions and material cross-fading between completely different geometry types and shader presets.
6. [06_audio_reactive_simulation.py](examples/06_audio_reactive_simulation.py) — Simulates audio-reactive animation by programmatically keyframing global parameters and knot attenuverters.
7. [07_headless_render.py](examples/07_headless_render.py) — Sets up and renders a sequence of frames headlessly using Blender's background CLI.
8. [08_programmatic_bake_export.py](examples/08_programmatic_bake_export.py) — Invokes the bake/export operator programmatically to compile procedural curves into standalone mesh frames.
To run any example from the command line:
**PowerShell (Windows):**
```powershell
& "C:\Program Files\Blender Foundation\Blender 5.0\blender.exe" --python examples/01_simple_torus_knot.py
```
**CMD (Windows):**
```cmd
"C:\Program Files\Blender Foundation\Blender 5.0\blender.exe" --python examples/01_simple_torus_knot.py
```
---
## Baking for Render Farms
The **Bake & Export** button (bottom of the Pr3tz panel) converts the procedural animation into a standalone `.blend`:
@@ -79,23 +143,24 @@ The **Bake & Export** button (bottom of the Pr3tz panel) converts the procedural
4. Submit the resulting `.blend` file(s) to your render farm — no addon needed on the farm.
---
## Module Layout
## Repository Layout
```
knot_animation/
├── __init__.py # Add-on entry point, bl_info, register/unregister
├── constants.py # Default playlist configs, camera/light positions, name tags
├── types.py # KnotConfig TypedDict (shared data contract)
├── compat.py # Headless align_matrix fix and other Blender compat patches
├── properties.py # Blender PropertyGroups (KnotItem, KnotGlobalSettings, …)
├── operators.py # KNOT_OT_* operators (Add, Remove, Move, Generate, Fit…)
├── ui.py # N-panel, UILists, draw_knot_properties()
├── geometry.py # _make_torus_knot() — pure parametric NURBS, no bpy.context reads
├── materials.py # 20 shader builders, material cache, blend/transition system
├── handler.py # @persistent frame_change_post handler, easing, playlist timing
├── scene_setup.py # setup_scene() — camera, area light, world, render settings
└── bake_export.py # KNOT_OT_BakeExport — bake to standalone .blend
├── pr3tz/ # Main add-on package folder
├── __init__.py # Add-on entry point, bl_info, register/unregister
├── constants.py # Default playlist configs, camera/light positions, name tags
├── types.py # KnotConfig TypedDict (shared data contract)
├── compat.py # Headless align_matrix fix and other Blender compat patches
├── properties.py # Blender PropertyGroups (KnotItem, KnotGlobalSettings, …)
├── operators.py # KNOT_OT_* operators (Add, Remove, Move, Generate, Fit…)
├── ui.py # N-panel, UILists, draw_knot_properties()
├── geometry.py # _make_torus_knot() — pure parametric NURBS, no bpy.context reads
├── materials.py # 20 shader builders, material cache, blend/transition system
├── handler.py # @persistent frame_change_post handler, easing, playlist timing
├── scene_setup.py # setup_scene() — camera, area light, world, render settings
└── bake_export.py # KNOT_OT_BakeExport — bake to standalone .blend
├── run_script.py # Bootstrap runner for Text Editor / CLI execution
└── package_addon.py # Automated packaging tool to create release .zip files
```
---
@@ -157,6 +222,26 @@ knot_animation/
| `PEARLESCENT` | Angle-dependent pearl sheen |
| `PLASMA_GLOW` | High-energy plasma emission |
---
## Release Packaging
We provide an automated script to pack the addon for release:
**PowerShell / CMD (Windows):**
```cmd
python package_addon.py
```
**Bash (macOS/Linux):**
```bash
python3 package_addon.py
```
This script:
1. Parses the release version dynamically from `pr3tz/__init__.py`.
2. Zips the `pr3tz/` directory into `releases/pr3tz_v<version>.zip`.
3. Automatically excludes developer files (`__pycache__`, `.git`, `.pyc`, etc.) so that the release package remains clean.
---
## Contributing
@@ -165,13 +250,13 @@ Pull requests are welcome. For major changes please open an issue first.
### Development setup
```powershell
# Clone
To configure your workspace for active development:
1. Clone the repository:
```bash
git clone https://github.com/YOUR_USERNAME/pr3tz.git
# Run the syntax checker (no Blender needed)
python check_syntax.py
```
2. Symlink the addon to Blender's addons folder as shown in **Option B** of the installation instructions.
3. Use `run_script.py` to quickly test code changes from the CLI or Text Editor.
---
+72
View File
@@ -0,0 +1,72 @@
import sys
import os
import bpy
# 1. Resolve path to include workspace root so pr3tz can be imported
dir_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if dir_path not in sys.path:
sys.path.append(dir_path)
import pr3tz
# 2. Register pr3tz addon
try:
if "pr3tz" in sys.modules:
import importlib
importlib.reload(pr3tz)
else:
import pr3tz
pr3tz.register()
print("[Pr3tz] Registered successfully!")
except Exception as e:
print(f"[Pr3tz] Failed to register: {e}")
sys.exit(1)
# 3. Configure the scene (camera, lights, timeline, etc.)
pr3tz.setup_scene()
# 4. Clear existing playlist
scene = bpy.context.scene
scene.knot_list.clear()
# 5. Add a new knot configuration
item = scene.knot_list.add()
item.name = "Trefoil Knot"
item.shape_type = 'TORUS_KNOT'
# Topology (coprime p and q)
item.torus_p = 2 # Revolutions around the torus axis
item.torus_q = 3 # Spins around the torus tube
# Dimensions
item.mode = 'MAJOR_MINOR'
item.torus_R = 2.0 # Distance from center to tube center
item.torus_r = 0.8 # Radius of the tube
# Geometry
item.geo_bDepth = 0.08 # Bevel depth (thickness of the curve tube)
# Material settings
item.material_mode = 'PRESET'
item.shader_id = 'GLOSS_BLUE'
item.preset_color = (0.2, 0.6, 1.0)
item.preset_roughness = 0.1
item.preset_metallic = 0.2
# 6. Fit timeline so the knot plays
scene.knot_globals.frames_per_knot = 120
bpy.ops.knot.fit_timeline()
# 7. Force instant geometry generation for current frame
pr3tz.knot_frame_handler(scene)
print("\n=====================================================================")
print("Example 1: Simple Torus Knot Loaded!")
print("=====================================================================")
print("Generated a 3D Trefoil Knot (p=2, q=3) using the 'GLOSS_BLUE' preset.")
print("Interact with it in Blender:")
print(" - Open the 'Pr3tz' N-Panel in the 3D Viewport sidebar.")
print(" - Change 'Revolutions (p)' or 'Spins (q)' to see the knot shape morph.")
print(" - Modify the 'Bevel Depth' to change its thickness.")
print("=====================================================================\n")
+70
View File
@@ -0,0 +1,70 @@
import sys
import os
import bpy
# 1. Resolve path to include workspace root so pr3tz can be imported
dir_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if dir_path not in sys.path:
sys.path.append(dir_path)
import pr3tz
# 2. Register pr3tz addon
try:
if "pr3tz" in sys.modules:
import importlib
importlib.reload(pr3tz)
else:
import pr3tz
pr3tz.register()
print("[Pr3tz] Registered successfully!")
except Exception as e:
print(f"[Pr3tz] Failed to register: {e}")
sys.exit(1)
# 3. Configure the scene (camera, lights, timeline, etc.)
pr3tz.setup_scene()
# 4. Clear existing playlist
scene = bpy.context.scene
scene.knot_list.clear()
# 5. Add a multi-link knot configuration
item = scene.knot_list.add()
item.name = "Interlocking Solomon Link"
item.shape_type = 'TORUS_KNOT'
# Topology with GCD > 1
item.torus_p = 4 # 4 revolutions
item.torus_q = 6 # 6 spins. gcd(4, 6) = 2 separate interlocking links
item.multiple_links = True # Enable rendering of all links as separate splines
# Dimensions & Geometry
item.torus_R = 2.5
item.torus_r = 1.0
item.geo_bDepth = 0.05
# Use a bright emissive neon color
item.material_mode = 'PRESET'
item.shader_id = 'NEON_GLOW'
item.preset_color = (0.0, 1.0, 0.9) # Cyan
item.preset_emission_strength = 3.0
# 6. Fit timeline so the knot plays
scene.knot_globals.frames_per_knot = 120
bpy.ops.knot.fit_timeline()
# 7. Force instant geometry generation for current frame
pr3tz.knot_frame_handler(scene)
print("\n=====================================================================")
print("Example 2: Multi-Link Knot Loaded!")
print("=====================================================================")
print("Generated a 3D Solomon Link (p=4, q=6) with multiple_links=True.")
print("This renders as 2 distinct interlocking/nested splines.")
print("Interact with it in Blender:")
print(" - Open the 'Pr3tz' N-Panel.")
print(" - Toggle 'Multiple Links' off/on to see the difference between")
print(" rendering a single loop vs. all mathematical links.")
print("=================================================================\n")
+89
View File
@@ -0,0 +1,89 @@
import sys
import os
import bpy
# 1. Resolve path to include workspace root so pr3tz can be imported
dir_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if dir_path not in sys.path:
sys.path.append(dir_path)
import pr3tz
# 2. Register pr3tz addon
try:
if "pr3tz" in sys.modules:
import importlib
importlib.reload(pr3tz)
else:
import pr3tz
pr3tz.register()
print("[Pr3tz] Registered successfully!")
except Exception as e:
print(f"[Pr3tz] Failed to register: {e}")
sys.exit(1)
# 3. Configure the scene (camera, lights, timeline, etc.)
pr3tz.setup_scene()
# 4. Clear existing playlist
scene = bpy.context.scene
scene.knot_list.clear()
# ── 1. Mobius Strip Winding ──
mobius_item = scene.knot_list.add()
mobius_item.name = "Mobius Strip Boundary"
mobius_item.shape_type = 'MOBIUS'
mobius_item.mobius_twists = 3 # Odd twist count creates single continuous loop
mobius_item.mobius_width = 1.2
mobius_item.torus_R = 2.5 # Uses major radius for ring dimension
mobius_item.geo_bDepth = 0.04
mobius_item.material_mode = 'PRESET'
mobius_item.shader_id = 'METALLIC'
mobius_item.preset_color = (1.0, 0.78, 0.28) # Gold
# ── 2. Lissajous 3D Curve ──
liss_item = scene.knot_list.add()
liss_item.name = "3D Lissajous Knot"
liss_item.shape_type = 'LISSAJOUS'
liss_item.liss_kx = 3 # X frequency
liss_item.liss_ky = 2 # Y frequency
liss_item.liss_kz = 5 # Z frequency
liss_item.liss_amp = 2.5
liss_item.geo_bDepth = 0.06
liss_item.material_mode = 'PRESET'
liss_item.shader_id = 'LAVA'
liss_item.preset_color = (1.0, 0.1, 0.0)
# ── 3. Spherical Spiral (Loxodrome) ──
spiral_item = scene.knot_list.add()
spiral_item.name = "Spherical Spiral"
spiral_item.shape_type = 'SPIRAL'
spiral_item.spiral_turns = 16
spiral_item.spiral_R = 2.2
# Open spiral (not cyclic)
spiral_item.geo_bDepth = 0.05
spiral_item.material_mode = 'PRESET'
spiral_item.shader_id = 'IRIDESCENT'
# 5. Set globals and fit timeline
glob = scene.knot_globals
glob.frames_per_knot = 100
bpy.ops.knot.fit_timeline()
# 6. Force instant geometry generation for current frame
pr3tz.knot_frame_handler(scene)
print("\n=====================================================================")
print("Example 3: Shape Types Playlist Loaded!")
print("=====================================================================")
print("Created a playlist containing:")
print(" 1. Mobius Strip Winding (Gold Metallic)")
print(" 2. 3D Lissajous Curve (Emissive Lava)")
print(" 3. Spherical Spiral (Iridescent Rainbow)")
print("=====================================================================")
print("Interact with it in Blender:")
print(" - Play the animation (Space) or scrub the timeline.")
print(" - Each shape will display in sequence.")
print(" - Adjust specific shape properties in the N-Panel sidebar.")
print("=====================================================================\n")
+100
View File
@@ -0,0 +1,100 @@
import sys
import os
import bpy
# 1. Resolve path to include workspace root so pr3tz can be imported
dir_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if dir_path not in sys.path:
sys.path.append(dir_path)
import pr3tz
# 2. Register pr3tz addon
try:
if "pr3tz" in sys.modules:
import importlib
importlib.reload(pr3tz)
else:
import pr3tz
pr3tz.register()
print("[Pr3tz] Registered successfully!")
except Exception as e:
print(f"[Pr3tz] Failed to register: {e}")
sys.exit(1)
# 3. Configure the scene (camera, lights, timeline, etc.)
pr3tz.setup_scene()
# 4. Clear existing playlist
scene = bpy.context.scene
scene.knot_list.clear()
# ── Style 1: Tube Spin Winding ──
# Animates the knot twisting around its own tube geometry.
item_spin = scene.knot_list.add()
item_spin.name = "Tube Spin Animation"
item_spin.shape_type = 'TORUS_KNOT'
item_spin.torus_p = 3
item_spin.torus_q = 5
item_spin.spin_phase_rate = 0.05 # Radians spin rate per frame
item_spin.material_mode = 'PRESET'
item_spin.shader_id = 'CARBON_FIBER'
item_spin.preset_color = (0.3, 0.3, 0.3)
# ── Style 2: Orbital Revolution ──
# Animates the entire knot revolving/orbiting around the core torus axis.
item_orbit = scene.knot_list.add()
item_orbit.name = "Orbital Orbit Animation"
item_orbit.shape_type = 'TORUS_KNOT'
item_orbit.torus_p = 3
item_orbit.torus_q = 5
item_orbit.rev_phase_rate = 0.03 # Radians revolution rate per frame
item_orbit.material_mode = 'PRESET'
item_orbit.shader_id = 'GLASS'
item_orbit.preset_color = (0.7, 0.9, 1.0)
# ── Style 3: Vertical Height Breathing ──
# Vertically stretches and squashes the knot structure.
item_breath = scene.knot_list.add()
item_breath.name = "Height Breathing Pulse"
item_breath.shape_type = 'TORUS_KNOT'
item_breath.torus_p = 3
item_breath.torus_q = 5
item_breath.height_rate = 0.06 # Sine frequency for height pulse
item_breath.material_mode = 'PRESET'
item_breath.shader_id = 'PLASMA_GLOW'
item_breath.preset_color = (1.0, 0.1, 0.7)
# ── Style 4: Uniform Scale Pulsation ──
# Animates the overall scale of the knot using a sine multiplier.
item_scale = scene.knot_list.add()
item_scale.name = "Scale Pulsation"
item_scale.shape_type = 'TORUS_KNOT'
item_scale.torus_p = 3
item_scale.torus_q = 5
item_scale.scale_amplitude = 0.25 # Scale factor oscillation amplitude (+/- 25%)
item_scale.scale_rate = 0.08 # Frequency of scale pulsation
item_scale.material_mode = 'PRESET'
item_scale.shader_id = 'ZEBRA_STRIPES'
# 5. Fit timeline
scene.knot_globals.frames_per_knot = 150
bpy.ops.knot.fit_timeline()
# 6. Force instant geometry generation for current frame
pr3tz.knot_frame_handler(scene)
print("\n=====================================================================")
print("Example 4: Animation Styles Playlist Loaded!")
print("=====================================================================")
print("Created 4 separate entries showing off isolated animation rates:")
print(" 1. Tube Spin (Carbon Fiber) — spin_phase_rate=0.05")
print(" 2. Orbital Orbit (Glass) — rev_phase_rate=0.03")
print(" 3. Height Breathing (Plasma) — height_rate=0.06")
print(" 4. Scale Pulsation (Zebra) — scale_amplitude=0.25, scale_rate=0.08")
print("=====================================================================")
print("Interact with it in Blender:")
print(" - Play the animation (Space) to see how the rates animate.")
print(" - Adjust the global 'Reactivity' slider to scale these speeds.")
print("=====================================================================\n")
+104
View File
@@ -0,0 +1,104 @@
import sys
import os
import bpy
# 1. Resolve path to include workspace root so pr3tz can be imported
dir_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if dir_path not in sys.path:
sys.path.append(dir_path)
import pr3tz
# 2. Register pr3tz addon
try:
if "pr3tz" in sys.modules:
import importlib
importlib.reload(pr3tz)
else:
import pr3tz
pr3tz.register()
print("[Pr3tz] Registered successfully!")
except Exception as e:
print(f"[Pr3tz] Failed to register: {e}")
sys.exit(1)
# 3. Configure the scene (camera, lights, timeline, etc.)
pr3tz.setup_scene()
# 4. Clear existing playlist
scene = bpy.context.scene
scene.knot_list.clear()
# ── Knot 1: Trefoil (Torus Knot) with Lava Preset ──
item_a = scene.knot_list.add()
item_a.name = "Trefoil Knot"
item_a.shape_type = 'TORUS_KNOT'
item_a.torus_p = 2
item_a.torus_q = 3
item_a.torus_R = 2.0
item_a.torus_r = 0.8
item_a.geo_bDepth = 0.05
item_a.material_mode = 'PRESET'
item_a.shader_id = 'LAVA'
item_a.preset_color = (1.0, 0.2, 0.0)
item_a.cycle_rate = 1.0
# ── Knot 2: 3D Lissajous with Glass Preset (Morphed using QUAD_IN_OUT) ──
# Triggers a transition morph when transitioning from item_a to item_b
item_b = scene.knot_list.add()
item_b.name = "Lissajous Morph Target"
item_b.shape_type = 'LISSAJOUS'
item_b.liss_kx = 3
item_b.liss_ky = 2
item_b.liss_kz = 4
item_b.liss_amp = 2.2
item_b.geo_bDepth = 0.08
item_b.material_mode = 'PRESET'
item_b.shader_id = 'GLASS'
item_b.preset_color = (0.3, 0.8, 1.0)
item_b.preset_roughness = 0.0
item_b.cycle_rate = 1.0
item_b.transition_frames = 60 # Morphs over 60 frames
item_b.transition_easing = 'QUAD_IN_OUT' # Smooth ease-in, ease-out curve
# ── Knot 3: Mobius Loop with Lichen Preset (Morphed using SMOOTHSTEP) ──
item_c = scene.knot_list.add()
item_c.name = "Mobius Morph Target"
item_c.shape_type = 'MOBIUS'
item_c.mobius_twists = 1
item_c.mobius_width = 1.6
item_c.torus_R = 2.4
item_c.geo_bDepth = 0.04
item_c.material_mode = 'PRESET'
item_c.shader_id = 'COPPER_PATINA'
item_c.preset_color = (0.1, 0.7, 0.5)
item_c.cycle_rate = 1.0
item_c.transition_frames = 40 # Morphs over 40 frames
item_c.transition_easing = 'SMOOTHSTEP' # S-curve ease
# 5. Set up playlist blend materials.
# MUST call this whenever modifying transitions or colors programmatically,
# so the shader node trees are rebuilt.
pr3tz.materials.prewarm_materials_and_blends(scene)
# 6. Fit timeline
scene.knot_globals.frames_per_knot = 120
bpy.ops.knot.fit_timeline()
# 7. Force instant geometry generation for current frame
pr3tz.knot_frame_handler(scene)
print("\n=====================================================================")
print("Example 5: Morph Transitions Playlist Loaded!")
print("=====================================================================")
print("Created 3 distinct shapes with custom transition settings:")
print(" - Trefoil Knot (Lava Preset)")
print(" - Lissajous (Glass Preset) morphs from Trefoil over 60 frames (Quad In/Out)")
print(" - Mobius Loop (Copper Patina) morphs from Lissajous over 40 frames (Smoothstep)")
print("=====================================================================")
print("Interact with it in Blender:")
print(" - Play the animation (Space).")
print(" - Watch how the geometries blend smoothly between shapes.")
print(" - Observe the material node network transition from Lava to Glass/Copper.")
print("=====================================================================\n")
+122
View File
@@ -0,0 +1,122 @@
import sys
import os
import math
import random
import bpy
# 1. Resolve path to include workspace root so pr3tz can be imported
dir_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if dir_path not in sys.path:
sys.path.append(dir_path)
import pr3tz
# 2. Register pr3tz addon
try:
if "pr3tz" in sys.modules:
import importlib
importlib.reload(pr3tz)
else:
import pr3tz
pr3tz.register()
print("[Pr3tz] Registered successfully!")
except Exception as e:
print(f"[Pr3tz] Failed to register: {e}")
sys.exit(1)
# 3. Configure the scene (camera, lights, timeline, etc.)
pr3tz.setup_scene()
# 4. Clear existing playlist
scene = bpy.context.scene
scene.knot_list.clear()
# 5. Add a base knot configuration
item = scene.knot_list.add()
item.name = "Audio Reactive Knot"
item.shape_type = 'TORUS_KNOT'
item.torus_p = 3
item.torus_q = 7
item.torus_R = 2.0
item.torus_r = 0.8
item.geo_bDepth = 0.05
# We will modulate minor radius and bevel depth using attenuverters.
# These variables will act as targets for our keyframed modulators.
item.mod_torus_r = 0.0 # Starts flat
item.mod_geo_bDepth = 0.0
item.material_mode = 'PRESET'
item.shader_id = 'NEON_GLOW'
item.preset_color = (1.0, 0.0, 0.5) # Magenta
item.preset_emission_strength = 2.0
# 6. Fit timeline
scene.frame_start = 1
scene.frame_end = 240
scene.render.fps = 24
# Set up prewarmed materials
pr3tz.materials.prewarm_materials_and_blends(scene)
# 7. Generate a simulated audio track (bass peaks on beats, high-hat noise)
# We will write keyframes to global properties to drive the animation.
glob = scene.knot_globals
# Clear any existing animation on globals
if glob.animation_data:
glob.animation_data.clear()
if item.animation_data:
item.animation_data.clear()
print("Simulating audio analysis and writing keyframes...")
# Loop over the frames and compute simulated audio values
for frame in range(scene.frame_start, scene.frame_end + 1):
# Beat occurs every 24 frames (1 second at 24fps)
beat_timer = (frame - 1) % 24
# Bass amplitude: sharp spike decaying exponentially
bass_amp = math.exp(-beat_timer * 0.15) if beat_timer < 20 else 0.0
# High frequency noise (high-hats/snares)
random.seed(frame) # Deterministic per frame
treble_amp = 0.15 * random.random() if (frame % 8) < 3 else 0.02
# Total reactivity scales the spin/rates
glob.reactivity_factor = 1.0 + bass_amp * 4.0
glob.keyframe_insert(data_path="reactivity_factor", frame=frame)
# Bevel depth thickness spikes with the bass beat
glob.global_master_thickness = 1.0 + bass_amp * 0.8
glob.keyframe_insert(data_path="global_master_thickness", frame=frame)
# Emission spikes on treble highlights
glob.global_emission_multiplier = 1.0 + treble_amp * 20.0
glob.keyframe_insert(data_path="global_emission_multiplier", frame=frame)
# Turbulence (noise) peaks on beats
glob.global_turbulence = bass_amp * 0.25
glob.keyframe_insert(data_path="global_turbulence", frame=frame)
# Hue shifts continuously, jumping slightly on beats
glob.global_hue_shift = (frame * 0.002 + bass_amp * 0.05) % 1.0
glob.keyframe_insert(data_path="global_hue_shift", frame=frame)
# 8. Force instant geometry generation for current frame
pr3tz.knot_frame_handler(scene)
print("\n=====================================================================")
print("Example 6: Audio-Reactive Simulation Loaded!")
print("=====================================================================")
print("Generated a 240-frame animation simulating audio beat sync:")
print(" - 'reactivity_factor' & 'global_turbulence' spike on bass beats (every 24 frames).")
print(" - 'global_master_thickness' swells on beats.")
print(" - 'global_emission_multiplier' flashes on high-frequency treble ticks.")
print(" - 'global_hue_shift' rotates colors over time.")
print("=====================================================================")
print("Interact with it in Blender:")
print(" - Press Space to play.")
print(" - Look at the Graph Editor to see the custom audio curves keyframed.")
print("=====================================================================\n")
+93
View File
@@ -0,0 +1,93 @@
import sys
import os
import bpy
# 1. Resolve path to include workspace root so pr3tz can be imported
dir_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if dir_path not in sys.path:
sys.path.append(dir_path)
import pr3tz
# 2. Register pr3tz addon
try:
if "pr3tz" in sys.modules:
import importlib
importlib.reload(pr3tz)
else:
import pr3tz
pr3tz.register()
print("[Pr3tz] Registered successfully!")
except Exception as e:
print(f"[Pr3tz] Failed to register: {e}")
sys.exit(1)
# 3. Configure the scene (camera, lights, timeline, etc.)
pr3tz.setup_scene()
# 4. Clear existing playlist
scene = bpy.context.scene
scene.knot_list.clear()
# 5. Add a simple spinning knot configuration
item = scene.knot_list.add()
item.name = "Render Example Knot"
item.shape_type = 'TORUS_KNOT'
item.torus_p = 2
item.torus_q = 5
item.torus_R = 2.0
item.torus_r = 0.8
item.geo_bDepth = 0.06
item.spin_phase_rate = 0.02
item.material_mode = 'PRESET'
item.shader_id = 'NEON_GLOW'
item.preset_color = (0.0, 0.5, 1.0) # Neon blue
item.preset_emission_strength = 2.0
pr3tz.materials.prewarm_materials_and_blends(scene)
# 6. Configure rendering for a super-fast headless export
scene.render.engine = 'CYCLES'
scene.cycles.samples = 4 # Very low samples for quick testing
scene.cycles.use_denoising = False
# Render size (small for fast test rendering)
scene.render.resolution_x = 480
scene.render.resolution_y = 270
scene.render.resolution_percentage = 100
# Setup Output directory
output_dir = os.path.join(dir_path, "renders")
if not os.path.exists(output_dir):
os.makedirs(output_dir)
# Set base filename
scene.render.filepath = os.path.join(output_dir, "frame_")
scene.render.image_settings.file_format = 'PNG'
scene.render.image_settings.color_mode = 'RGBA'
# 7. Render a short range (10 frames)
start_frame = 1
end_frame = 10
print(f"\nHeadless Render Started. Rendering frames {start_frame} to {end_frame}...")
for f in range(start_frame, end_frame + 1):
print(f"Rendering frame {f}/{end_frame}...")
scene.frame_set(f) # Triggers knot_frame_handler automatically
# We construct the filepath for this frame manually to save it
scene.render.filepath = os.path.join(output_dir, f"frame_{f:04d}.png")
# Execute render
bpy.ops.render.render(write_still=True)
print(f"\nRendering complete! Output frames saved to: {output_dir}")
print("\n=====================================================================")
print("Example 7: Headless Render Script Completed!")
print("=====================================================================")
print("You can run this script from your terminal headlessly using:")
print(" blender -b -P examples/07_headless_render.py")
print("=====================================================================\n")
+84
View File
@@ -0,0 +1,84 @@
import sys
import os
import bpy
# 1. Resolve path to include workspace root so pr3tz can be imported
dir_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if dir_path not in sys.path:
sys.path.append(dir_path)
import pr3tz
# 2. Register pr3tz addon
try:
if "pr3tz" in sys.modules:
import importlib
importlib.reload(pr3tz)
else:
import pr3tz
pr3tz.register()
print("[Pr3tz] Registered successfully!")
except Exception as e:
print(f"[Pr3tz] Failed to register: {e}")
sys.exit(1)
# 3. Configure the scene (camera, lights, timeline, etc.)
pr3tz.setup_scene()
# 4. Clear existing playlist and populate with a couple of animated shapes
scene = bpy.context.scene
scene.knot_list.clear()
item_1 = scene.knot_list.add()
item_1.name = "Bake Target A"
item_1.shape_type = 'TORUS_KNOT'
item_1.torus_p = 2
item_1.torus_q = 3
item_1.spin_phase_rate = 0.05
item_1.material_mode = 'PRESET'
item_1.shader_id = 'LAVA'
item_2 = scene.knot_list.add()
item_2.name = "Bake Target B"
item_2.shape_type = 'LISSAJOUS'
item_2.liss_kx = 3
item_2.liss_ky = 2
item_2.liss_amp = 2.0
item_2.material_mode = 'PRESET'
item_2.shader_id = 'GLASS'
item_2.transition_frames = 20
pr3tz.materials.prewarm_materials_and_blends(scene)
# 5. Set a short frame range for rapid baking
scene.frame_start = 1
scene.frame_end = 40
scene.render.fps = 24
# Setup target filepath for the baked blend file
baked_dir = os.path.join(dir_path, "baked")
if not os.path.exists(baked_dir):
os.makedirs(baked_dir)
output_blend = os.path.join(baked_dir, "baked_knot_animation.blend")
# 6. Execute programmatic bake and export
print(f"\nStarting programmatic bake & export to: {output_blend}...")
# Run the operator directly, passing parameters.
# Since it inherits from ExportHelper, it expects filepath to be passed.
bpy.ops.knot.bake_export(
filepath=output_blend,
use_render_resolution=False, # Set to False to bake quickly at preview res
pack_textures=True,
split_export=False
)
print(f"\nBake completed! stand-alone .blend file saved at: {output_blend}")
print("\n=====================================================================")
print("Example 8: Programmatic Bake & Export Script Completed!")
print("=====================================================================")
print("The procedural knot was successfully baked to standalone meshes.")
print("The output .blend file requires no addons and can be rendered")
print("on any computer or render farm (e.g. SheepIt).")
print("=====================================================================\n")
+67
View File
@@ -0,0 +1,67 @@
import os
import re
import zipfile
def get_addon_version():
"""Reads pr3tz/__init__.py and extracts the bl_info version tuple."""
init_path = os.path.join("pr3tz", "__init__.py")
if not os.path.exists(init_path):
raise FileNotFoundError(f"Could not find {init_path}")
with open(init_path, "r", encoding="utf-8") as f:
content = f.read()
# Search for the "version": (x, y, z) pattern in bl_info
match = re.search(r'"version"\s*:\s*\(([^)]+)\)', content)
if not match:
raise ValueError("Could not parse version from bl_info in __init__.py")
version_str = match.group(1)
# Parse numbers, e.g. "2, 0, 0" -> ["2", "0", "0"] -> "2.0.0"
version_parts = [part.strip() for part in version_str.split(",")]
return ".".join(version_parts)
def package_addon():
"""Zips the pr3tz addon folder, excluding development artifacts."""
try:
version = get_addon_version()
except Exception as e:
print(f"Warning: Could not parse version: {e}. Defaulting to 'unknown'.")
version = "unknown"
release_dir = "releases"
if not os.path.exists(release_dir):
os.makedirs(release_dir)
print(f"Created output directory: {release_dir}/")
zip_filename = f"pr3tz_v{version}.zip"
zip_path = os.path.join(release_dir, zip_filename)
print(f"Packaging addon v{version} into {zip_path}...")
addon_dir = "pr3tz"
ignored_dirs = {"__pycache__", ".git"}
ignored_extensions = {".pyc", ".pyo", ".py.bak"}
added_count = 0
with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
for root, dirs, files in os.walk(addon_dir):
# Prune directories in-place to prevent walking into them
dirs[:] = [d for d in dirs if d not in ignored_dirs]
for file in files:
_, ext = os.path.splitext(file)
if ext in ignored_extensions:
continue
file_path = os.path.join(root, file)
# Ensure the path in the ZIP has "pr3tz/" as its root folder
arcname = os.path.relpath(file_path, os.path.dirname(addon_dir))
zipf.write(file_path, arcname)
print(f" Added: {arcname}")
added_count += 1
print(f"\nSuccess! Packaged {added_count} files into {zip_path}")
if __name__ == "__main__":
package_addon()
+12 -10
View File
@@ -1,6 +1,6 @@
"""
knot_animation/__init__.py
--------------------------
pr3tz/__init__.py
-----------------
Blender add-on entry-point for the Pr3tz procedural animation system.
What this add-on does
@@ -42,16 +42,18 @@ How to use
----------
Requires Blender 5.0 or later.
Option A Blender Text Editor:
Open knot_animation/__init__.py, click Run Script (Alt+P).
Option A Persistent add-on:
Zip the pr3tz/ folder (using package_addon.py) and install it from Preferences.
Alternatively, copy or symlink the pr3tz/ folder to Blender's addons directory
and enable it from Edit → Preferences → Add-ons.
The panel appears under the "Pr3tz" tab in the 3-D viewport N-panel.
Option B Command line:
blender.exe --python knot_animation/__init__.py
Option B Run as a script (via bootstrap runner):
Open run_script.py from the workspace root in Blender's Text Editor,
and click Run Script (Alt+P).
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
"Pr3tz" tab in the 3-D viewport N-panel.
Option C Command line:
blender.exe --python run_script.py
"""
if "bpy" in locals():
+2 -2
View File
@@ -149,8 +149,8 @@ class KNOT_OT_BakePreview(bpy.types.Operator):
glob = context.scene.knot_globals
_make_torus_knot(
config,
resolution=glob.resolution,
bevel_resolution=glob.bevel_resolution,
resolution=glob.preview_resolution,
bevel_resolution=glob.preview_bevel_resolution,
knot_scale=glob.knot_scale,
scene=context.scene,
)
+41
View File
@@ -0,0 +1,41 @@
import sys
import os
import bpy
# Get the directory of this script (workspace root) and append it to sys.path
dir_path = os.path.dirname(os.path.abspath(__file__))
if dir_path not in sys.path:
sys.path.append(dir_path)
# Try to register the pr3tz module
try:
# If the module was already imported, reload it to pick up changes
if "pr3tz" in sys.modules:
import importlib
import pr3tz
# Trigger reload inside pr3tz to refresh its submodules
importlib.reload(pr3tz)
else:
import pr3tz
# Unregister first to clean up any leftover handlers or UI components
try:
pr3tz.unregister()
except Exception:
pass
# Register the addon components
pr3tz.register()
# Run the initial scene setup (creates camera, light, and default playlist)
pr3tz.setup_scene()
# Generate the torus knot for the current frame immediately
pr3tz.knot_frame_handler(bpy.context.scene)
print("[Pr3tz] Registered and scene set up successfully!")
print("[Pr3tz] Scrub the timeline or press Space in the Viewport to animate.")
except Exception as e:
print(f"[Pr3tz] Failed to register: {e}")
import traceback
traceback.print_exc()