Recent Changes - Search:

Blender Technical Director?

pmwiki.org

edit SideBar

BTD /

Lab2

BTD/Modul 2: Übungen

Aufgabe 1: Automatisiertes Asset-Erzeugungsskript

  • Parametrisches Objekt erzeugen (z.B. Würfel oder Zylinder).
  • Parameter: Größe, Segmente, Materialzuweisung.
  • Objekte automatisch in einer neuen Collection organisieren.

Aufgabe 2: Custom Operator und Panel

  • Erstellen Sie einen Blender Custom Operator, der Objekte verschiebt.
  • Panel hinzufügen, das Parameter anzeigt (z.B. Verschiebe-Distanz).
  • Bonus: Undo-Funktion implementieren.

Aufgabe 3: Batch Rendering

  • Script zum Rendern aller Szenen im Blend-File erstellen.
  • Jede Szene in einem eigenen Unterordner speichern.
  • Logging: Dateipfade und Renderzeiten in Konsole ausgeben.

Material

Virtuelle Kamera in Blender

Websocket-Client: https://btd.kalisz.co/demos/2_lab_virtualcamera.html
NOTE: Browsers can only access the sensors on https://. If using TLS, you need to make sure that the Websockets are also using wss://, but for this the websocket server needs to be secured as well. This is a common pitfall!
NOTE: Another pitfall is that you need to accept the websocket certificate in the browser by navigating to the websocket server using https://, so navigate the browser to https://{IP_OF_BLENDER_SERVER:PORT} and accept the self-signed certificate.

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 &#8594; 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 &#8594; 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()


Edit - History - Print - Recent Changes - Search
Page last modified on January 03, 2026, at 10:44 PM UTC