|
BTD /
Lab2BTD/Modul 2: ÜbungenAufgabe 1: Automatisiertes Asset-Erzeugungsskript
Aufgabe 2: Custom Operator und Panel
Aufgabe 3: Batch Rendering
MaterialVirtuelle Kamera in BlenderWebsocket-Client: https://btd.kalisz.co/demos/2_lab_virtualcamera.html Python: Generate SSL-Certificates
import os
import datetime
from cryptography import x509
from cryptography.x509.oid import NameOID
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import rsa
def generate_self_signed_cert(cert_path, key_path, hostname="localhost"):
key = rsa.generate_private_key(
public_exponent=65537,
key_size=2048,
)
subject = issuer = x509.Name([
x509.NameAttribute(NameOID.COMMON_NAME, hostname),
])
cert = (
x509.CertificateBuilder()
.subject_name(subject)
.issuer_name(issuer)
.public_key(key.public_key())
.serial_number(x509.random_serial_number())
.not_valid_before(datetime.datetime.utcnow() - datetime.timedelta(days=1))
.not_valid_after(datetime.datetime.utcnow() + datetime.timedelta(days=365))
.add_extension(
x509.SubjectAlternativeName([
x509.DNSName(hostname),
x509.DNSName("localhost"),
]),
critical=False,
)
.sign(key, hashes.SHA256())
)
with open(key_path, "wb") as f:
f.write(
key.private_bytes(
serialization.Encoding.PEM,
serialization.PrivateFormat.TraditionalOpenSSL,
serialization.NoEncryption(),
)
)
with open(cert_path, "wb") as f:
f.write(cert.public_bytes(serialization.Encoding.PEM))
CERT_DIR = "./"
CERT_FILE = os.path.join(CERT_DIR, "imu_cert.pem")
KEY_FILE = os.path.join(CERT_DIR, "imu_key.pem")
if not os.path.exists(CERT_FILE):
generate_self_signed_cert(CERT_FILE, KEY_FILE, hostname="0.0.0.0")
HTML/Javascript: Websocket-Client
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>IMU → Blender WebSocket Controller</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body {
font-family: Arial, sans-serif;
background: #111;
color: #eee;
padding: 10px;
}
input, button {
font-size: 16px;
width: 100%;
margin: 5px 0;
}
label {
display: block;
margin-top: 10px;
}
pre {
background: #000;
padding: 10px;
max-height: 200px;
overflow-y: auto;
font-size: 12px;
}
.status {
margin: 5px 0;
font-weight: bold;
}
.ok { color: #0f0; }
.warn { color: #ff0; }
.err { color: #f00; }
</style>
</head>
<body>
<h2>IMU → Blender Controller</h2>
<label>
WebSocket Server (Blender):
<input id="wsUrl" placeholder="wss://192.168.178.30:8765" value="wss://192.168.178.30:8765">
</label>
<button id="connectBtn">Connect</button>
<label>
<input type="checkbox" id="useRotation" checked>
Use Rotation
</label>
<label>
<input type="checkbox" id="useTranslation" checked>
Use Translation
</label>
<div class="status" id="imuStatus"></div>
<div class="status" id="wsStatus"></div>
<h3>Debug Output</h3>
<pre id="debug"></pre>
<script>
let ws = null;
let lastData = {};
const debug = msg => {
const el = document.getElementById("debug");
el.textContent = msg + "\n" + el.textContent;
};
const imuStatus = document.getElementById("imuStatus");
const wsStatus = document.getElementById("wsStatus");
// --- Capability Checks ---
const hasOrientation = "DeviceOrientationEvent" in window;
const hasMotion = "DeviceMotionEvent" in window;
imuStatus.innerHTML =
`Orientation: ${hasOrientation ? "OK" : "NO"} | Motion: ${hasMotion ? "OK" : "NO"}`;
imuStatus.className = hasOrientation ? "status ok" : "status err";
// --- iOS Permission ---
async function requestIOSPermission() {
if (
typeof DeviceMotionEvent !== "undefined" &&
typeof DeviceMotionEvent.requestPermission === "function"
) {
const response = await DeviceMotionEvent.requestPermission();
debug("iOS permission: " + response);
}
}
// --- WebSocket ---
document.getElementById("connectBtn").onclick = async () => {
await requestIOSPermission();
const url = document.getElementById("wsUrl").value;
ws = new WebSocket(url);
ws.onopen = () => {
wsStatus.textContent = "WebSocket connected";
wsStatus.className = "status ok";
};
ws.onerror = err => {
wsStatus.textContent = "WebSocket error";
wsStatus.className = "status err";
debug(err);
};
ws.onclose = () => {
wsStatus.textContent = "WebSocket closed";
wsStatus.className = "status warn";
};
};
// --- IMU Handlers ---
window.addEventListener("deviceorientation", e => {
lastData.rotation = {
alpha: e.alpha || 0,
beta: e.beta || 0,
gamma: e.gamma || 0
};
});
window.addEventListener("devicemotion", e => {
if (e.accelerationIncludingGravity) {
lastData.translation = {
x: e.accelerationIncludingGravity.x || 0,
y: e.accelerationIncludingGravity.y || 0,
z: e.accelerationIncludingGravity.z || 0
};
}
});
// --- Send Loop ---
setInterval(() => {
if (!ws || ws.readyState !== WebSocket.OPEN) return;
const payload = {
useRotation: document.getElementById("useRotation").checked,
useTranslation: document.getElementById("useTranslation").checked,
rotation: lastData.rotation,
translation: lastData.translation,
timestamp: performance.now()
};
ws.send(JSON.stringify(payload));
debug(JSON.stringify(payload, null, 2));
}, 30);
</script>
</body>
</html>
Blender: Websocket Server NOTE: Replace CERT_DIR = "" with your local directory where you have stored the SSL certificates (cert.pem and key.pem)
bl_info = {
"name": "IMU WebSocket Controller",
"author": "TD Demo",
"version": (1, 1, 0),
"blender": (3, 6, 0),
"location": "View3D > Sidebar > IMU",
"category": "Animation",
}
import bpy
import asyncio
import json
import math
import threading
import time
import os
import websockets
import ssl
from mathutils import Vector, Euler
debug = {
"clients": 0,
"last_message_time": 0.0,
"packet_count": 0,
"last_packet": "",
"last_rotation": (0, 0, 0),
"last_translation": (0, 0, 0),
}
# --------------------------------------------------
# GLOBAL STATE
# --------------------------------------------------
state = {
"loop": None,
"thread": None,
"server": None,
"running": False,
"rotation": Vector((0, 0, 0)),
"translation": Vector((0, 0, 0)),
"zero_rot": Vector((0, 0, 0)),
"zero_pos": Vector((0, 0, 0)),
"imu": {
"pos": [0.0, 0.0, 0.0],
"rot": [0.0, 0.0, 0.0],
},
"use_position": True,
"use_rotation": True,
"target_object": "",
}
CERT_DIR = # "" # This will throw an error, because you need to put in your local directory where you have generated and stored the SSL-Certificates
CERT_FILE = os.path.join(CERT_DIR, "imu_cert.pem")
KEY_FILE = os.path.join(CERT_DIR, "imu_key.pem")
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
ssl_context.load_cert_chain(CERT_FILE, KEY_FILE)
# --------------------------------------------------
# APPLY IMU DATA (MAIN THREAD SAFE)
# --------------------------------------------------
def apply_imu(data):
props = bpy.context.scene.imu_props
obj = props.target
if not obj:
return
if props.use_rotation and "rotation" in data:
r = data["rotation"]
obj.rotation_euler = Euler(Vector((
math.radians(r["beta"]),
math.radians(r["gamma"]),
math.radians(r["alpha"]),
)) - state["zero_rot"])
if props.use_translation and "translation" in data:
t = data["translation"]
obj.location = Vector((
t["x"] * props.translation_scale,
t["y"] * props.translation_scale,
t["z"] * props.translation_scale,
)) - state["zero_pos"]
# --------------------------------------------------
# ASYNC SERVER
# --------------------------------------------------
async def imu_handler(websocket):
debug["clients"] += 1
try:
async for msg in websocket:
debug["packet_count"] += 1
debug["last_message_time"] = time.time()
debug["last_packet"] = msg[:200]
data = json.loads(msg)
if "rotation" in data:
r = data["rotation"]
debug["last_rotation"] = (
r.get("alpha", 0),
r.get("beta", 0),
r.get("gamma", 0),
)
if "translation" in data:
t = data["translation"]
debug["last_translation"] = (
t.get("x", 0),
t.get("y", 0),
t.get("z", 0),
)
bpy.app.timers.register(
lambda d=data: apply_imu(d),
first_interval=0.0
)
state["imu"]["pos"] = data.get("pos", state["imu"]["pos"])
state["imu"]["rot"] = data.get("rot", state["imu"]["rot"])
finally:
debug["clients"] -= 1
async def start_ws_server(port):
state["server"] = await websockets.serve(
imu_handler, "0.0.0.0", port, ssl=ssl_context
)
# Note: 0.0.0.0 binds on all local interfaces,
# so not only local host can connect
print(f"[IMU] Secure WebSocket running on wss://0.0.0.0:{port}")
async def stop_ws_server():
if state["server"]:
state["server"].close()
await state["server"].wait_closed()
state["server"] = None
print("[IMU] Secure WebSocket stopped")
# --------------------------------------------------
# THREAD MANAGEMENT
# --------------------------------------------------
def server_thread(port):
loop = asyncio.new_event_loop()
state["loop"] = loop
asyncio.set_event_loop(loop)
loop.run_until_complete(start_ws_server(port))
state["running"] = True
try:
loop.run_forever()
finally:
loop.run_until_complete(stop_ws_server())
loop.close()
state["running"] = False
# --------------------------------------------------
# OPERATORS
# --------------------------------------------------
class IMU_OT_Start(bpy.types.Operator):
bl_idname = "imu.start"
bl_label = "Start Server"
def execute(self, context):
if state["running"]:
self.report({'WARNING'}, "Server already running")
return {'CANCELLED'}
port = context.scene.imu_props.port
t = threading.Thread(
target=server_thread,
args=(port,),
daemon=True
)
state["thread"] = t
t.start()
return {'FINISHED'}
class IMU_OT_Stop(bpy.types.Operator):
bl_idname = "imu.stop"
bl_label = "Stop Server"
def execute(self, context):
if not state["running"]:
return {'CANCELLED'}
state["loop"].call_soon_threadsafe(state["loop"].stop)
return {'FINISHED'}
class IMU_OT_Zero(bpy.types.Operator):
bl_idname = "imu.zero"
bl_label = "Zero / Calibrate"
def execute(self, context):
state["zero_rot"] = state["rotation"].copy()
state["zero_pos"] = state["translation"].copy()
return {'FINISHED'}
# --------------------------------------------------
# PROPERTIES
# --------------------------------------------------
class IMUProperties(bpy.types.PropertyGroup):
port: bpy.props.IntProperty(default=8765)
target: bpy.props.PointerProperty(type=bpy.types.Object)
use_rotation: bpy.props.BoolProperty(default=True)
use_translation: bpy.props.BoolProperty(default=True)
translation_scale: bpy.props.FloatProperty(default=0.1)
# --------------------------------------------------
# UI PANEL
# --------------------------------------------------
class IMU_PT_Panel(bpy.types.Panel):
bl_label = "IMU Controller"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = "IMU"
def draw(self, context):
p = context.scene.imu_props
layout = self.layout
layout.prop(p, "target")
layout.prop(p, "port")
row = layout.row()
row.operator("imu.start")
row.operator("imu.stop")
layout.prop(p, "use_rotation")
layout.prop(p, "use_translation")
layout.prop(p, "translation_scale")
layout.operator("imu.zero")
class IMU_PT_Debug(bpy.types.Panel):
bl_label = "IMU Debug"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = "IMU"
bl_parent_id = "IMU_PT_Panel"
def draw(self, context):
layout = self.layout
p = context.scene.imu_props
layout.label(text=f"Server running: {'YES' if state['running'] else 'NO'}")
layout.label(text=f"Connected clients: {debug['clients']}")
layout.label(text=f"Packets received: {debug['packet_count']}")
if debug["last_message_time"] > 0:
dt = time.time() - debug["last_message_time"]
layout.label(text=f"Last packet: {dt:.2f}s ago")
layout.separator()
layout.label(text="Rotation (deg):")
layout.label(text=str(debug["last_rotation"]))
layout.label(text="Translation:")
layout.label(text=str(debug["last_translation"]))
layout.separator()
layout.label(text="Last packet:")
layout.box().label(text=debug["last_packet"])
# --------------------------------------------------
# REGISTER
# --------------------------------------------------
classes = (
IMUProperties,
IMU_OT_Start,
IMU_OT_Stop,
IMU_OT_Zero,
IMU_PT_Panel,
IMU_PT_Debug,
)
def register():
for c in classes:
bpy.utils.register_class(c)
bpy.types.Scene.imu_props = bpy.props.PointerProperty(type=IMUProperties)
def unregister():
if state["running"] and state["loop"]:
state["loop"].call_soon_threadsafe(state["loop"].stop)
for c in reversed(classes):
bpy.utils.unregister_class(c)
del bpy.types.Scene.imu_props
if __name__ == "__main__":
register()
|