changeset 2432:b8a408caf115

start blender sync
author drewp@bigasterisk.com
date Tue, 28 May 2024 15:35:12 -0700
parents d1946cb32121
children ffa69645e9dc
files src/light9/blender/__init__.py src/light9/blender/asyncio_thread.py src/light9/blender/light_control/__init__.py src/light9/blender/light_control/send_to_collector.py src/light9/blender/time_sync/__init__.py src/light9/blender/time_sync/time_from_graph.py
diffstat 6 files changed, 204 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/light9/blender/__init__.py	Tue May 28 15:35:12 2024 -0700
@@ -0,0 +1,37 @@
+# ln -s ./ ~/.config/blender/4.2/scripts/addons/light9_sync/
+
+#~/own/tool/blender $LIGHT9_SHOW/song1.blend --addons light9_sync
+import logging
+import sys
+
+from . import light_control, time_sync
+
+bl_info = {
+    "name": "light9_sync",
+    "description": "light9 sync",
+    "version": (0, 0, 1),
+    "blender": (4, 2, 0),
+    "category": "Object",
+}
+
+modules = (time_sync, light_control)
+
+
+def register():
+    logging.getLogger('autodepgraphapi').setLevel(logging.INFO)
+    logging.getLogger('syncedgraph').setLevel(logging.INFO)
+    for p in [
+            '/home/drewp/.local/share/pdm/venvs/light9-zxQNOBNq-3.11/lib/python3.11/site-packages',
+            '/home/drewp/projects/light9/src/',
+            '/my/proj/rdfdb',
+    ]:
+        if p not in sys.path:
+            sys.path.append(p)
+
+    for module in modules:
+        module.register()
+
+
+def unregister():
+    for module in reversed(modules):
+        module.unregister()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/light9/blender/asyncio_thread.py	Tue May 28 15:35:12 2024 -0700
@@ -0,0 +1,16 @@
+import asyncio
+from threading import Thread
+from typing import Coroutine
+
+
+def startLoopInThread(task: Coroutine) -> asyncio.AbstractEventLoop:
+
+    def start_background_loop(loop: asyncio.AbstractEventLoop) -> None:
+        asyncio.set_event_loop(loop)
+        loop.run_forever()
+
+    loop = asyncio.new_event_loop()
+    t = Thread(target=start_background_loop, args=(loop,), daemon=True)
+    t.start()
+    asyncio.run_coroutine_threadsafe(task, loop)
+    return loop
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/light9/blender/light_control/__init__.py	Tue May 28 15:35:12 2024 -0700
@@ -0,0 +1,28 @@
+"""
+watch blender lights, output to real lights
+"""
+
+import time
+from typing import cast
+
+import bpy  # type: ignore
+from bpy.app.handlers import persistent
+
+sender: object = None
+
+
+def register():
+    global sender
+    from .send_to_collector import Sender
+
+    sender = Sender()
+
+    # @persistent
+    # def fcp(scene):
+    #     cast(Sender, sender).on_frame_change_post(scene)
+
+    # bpy.app.handlers.frame_change_post.append(fcp)
+
+
+def unregister():
+    raise NotImplementedError
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/light9/blender/light_control/send_to_collector.py	Tue May 28 15:35:12 2024 -0700
@@ -0,0 +1,7 @@
+class Sender:
+
+    def blenderTime(self) -> float:
+        return -55
+
+    def on_frame_change_post(self, scene, *args):
+        print('ofcp',scene,args)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/light9/blender/time_sync/__init__.py	Tue May 28 15:35:12 2024 -0700
@@ -0,0 +1,16 @@
+"""
+sets blender time to track ascoltami time;
+blender-side time changes are sent back to ascoltami
+"""
+
+sync: object = None
+
+
+def register():
+    global sync
+    from .time_from_graph import Sync
+    sync = Sync()
+
+
+def unregister():
+    raise NotImplementedError
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/light9/blender/time_sync/time_from_graph.py	Tue May 28 15:35:12 2024 -0700
@@ -0,0 +1,100 @@
+import threading
+import time
+
+import bpy  # type: ignore
+from bpy.app.handlers import persistent  # type: ignore
+from light9.typedgraph import typedValue
+from rdfdb.syncedgraph.syncedgraph import SyncedGraph
+from rdflib import URIRef
+
+from light9 import networking
+from light9.blender.asyncio_thread import startLoopInThread
+from light9.namespaces import L9
+from light9.run_local import log
+
+
+def clamp(lo, hi, x):
+    return max(lo, min(hi, x))
+
+
+def floatValue(graph: SyncedGraph, s: URIRef, p: URIRef) -> float | None:
+    return typedValue(float | None, graph, s, p)
+
+
+UPDATE_PERIOD = 1 / 20
+
+
+class Sync:
+    lock = threading.Lock()
+    duration: float = 1
+    wallStartTime: float | None = None
+    pausedSongTime: float | None = None
+    playing=False
+    latestTime: float
+
+    def __init__(self):
+        # main thread
+        self.lastSetFrame = -1
+        self.lastGraphFrame = -1
+        self.frameSetBy = 'init'
+        startLoopInThread(self.task())
+        bpy.app.timers.register(self.update)
+        bpy.app.handlers.frame_change_post.append(self.on_frame_change_post)
+
+    ## read from graph
+
+    async def task(self):
+        # bg thread with asyncio loop
+        self.graph = SyncedGraph(networking.rdfdb.url, "time_sync")
+        self.graph.addHandler(self.syncFromGraph)
+
+    def syncFromGraph(self):
+        # bg thread
+        with self.lock:
+            asco = L9['ascoltami']
+            self.wallStartTime = floatValue(self.graph, asco, L9['wallStartTime'])
+            self.pausedSongTime = floatValue(self.graph, asco, L9['pausedSongTime'])
+            self.duration = floatValue(self.graph, asco, L9['duration']) or 1.0
+            self.playing = typedValue(bool, self.graph, asco, L9['playing'])
+
+    def currentTime(self) -> float | None:
+        if self.wallStartTime is not None:
+            return time.time() - self.wallStartTime
+        if self.pausedSongTime is not None:
+            return self.pausedSongTime
+        log.warn('no time data')
+        return None
+
+    ## graph time -> blender time
+
+    @persistent
+    def update(self):
+        # main thread? wherever blender runs timers
+        if self.playing:
+            with self.lock:
+                t = self.currentTime()
+            if t is not None:
+                self.setBlenderTime(t, self.duration)
+        return UPDATE_PERIOD
+
+    def setBlenderTime(self, t: float, duration: float):
+        scene = bpy.context.scene
+        fps = scene.render.fps
+        scene.frame_start = 0
+        scene.frame_end = int(duration * fps)
+        fr = int(clamp(t, 0, duration) * fps)
+        self.lastSetFrame = fr
+        scene.frame_set(fr)
+
+    ## blender time -> graph time
+
+    @persistent
+    def on_frame_change_post(self, scene, deps):
+        if scene.frame_current != self.lastSetFrame:
+            # self.setGraphTime(scene.frame_current / scene.render.fps, self.duration)
+            self.lastSetFrame = scene.frame_current
+            t = scene.frame_current / scene.render.fps
+            self.setInGraph(t)
+
+    def setInGraph(self, t: float):
+        log.warning(f'todo: set graph to {t}')
\ No newline at end of file