kopia lustrzana https://github.com/jameshball/osci-render
332 wiersze
11 KiB
Python
332 wiersze
11 KiB
Python
bl_info = {
|
|
"name": "osci-render",
|
|
"author": "James Ball",
|
|
"version": (1, 1, 0),
|
|
"blender": (3, 1, 2),
|
|
"location": "View3D",
|
|
"description": "Addon to send gpencil frames over to osci-render",
|
|
"warning": "Requires a camera and gpencil object",
|
|
"wiki_url": "https://github.com/jameshball/osci-render",
|
|
"category": "Development",
|
|
}
|
|
|
|
import bpy
|
|
import os
|
|
import bmesh
|
|
import socket
|
|
import json
|
|
import atexit
|
|
import struct
|
|
import base64
|
|
from bpy.props import StringProperty
|
|
from bpy.app.handlers import persistent
|
|
from bpy_extras.io_utils import ImportHelper
|
|
|
|
HOST = "localhost"
|
|
PORT = 51677
|
|
|
|
sock = None
|
|
|
|
|
|
GPLA_MAJOR = 2
|
|
GPLA_MINOR = 0
|
|
GPLA_PATCH = 0
|
|
|
|
|
|
class OBJECT_PT_osci_render_settings(bpy.types.Panel):
|
|
bl_idname = "OBJECT_PT_osci_render_settings"
|
|
bl_label = "osci-render settings"
|
|
bl_space_type = "PROPERTIES"
|
|
bl_region_type = "WINDOW"
|
|
bl_context = "render"
|
|
|
|
def draw_header(self, context):
|
|
layout = self.layout
|
|
|
|
def draw(self, context):
|
|
self.layout.prop(context.scene, "oscirenderPort")
|
|
global sock
|
|
if sock is None:
|
|
self.layout.operator("render.osci_render_connect", text="Connect to osci-render instance")
|
|
else:
|
|
self.layout.operator("render.osci_render_close", text="Close osci-render connection")
|
|
self.layout.operator("render.osci_render_save", text="Save line art to file")
|
|
|
|
class osci_render_connect(bpy.types.Operator):
|
|
bl_label = "Connect to osci-render"
|
|
bl_idname = "render.osci_render_connect"
|
|
bl_description = "Connect to osci-render"
|
|
|
|
def execute(self, context):
|
|
global sock
|
|
if sock is None:
|
|
try:
|
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
sock.settimeout(1)
|
|
sock.connect((HOST, context.scene.oscirenderPort))
|
|
send_scene_to_osci_render(bpy.context.scene)
|
|
except socket.error as exp:
|
|
sock = None
|
|
self.report({"WARNING"}, "Failed to connect to osci-render - make sure it is running first!")
|
|
return {"CANCELLED"}
|
|
|
|
return {"FINISHED"}
|
|
|
|
class osci_render_save(bpy.types.Operator, ImportHelper):
|
|
bl_label = "Save Line Art"
|
|
bl_idname = "render.osci_render_save"
|
|
bl_description = "Save line art to the chosen file"
|
|
filename_ext = ".gpla"
|
|
|
|
filter_glob: StringProperty(
|
|
default="*.gpla",
|
|
options={"HIDDEN"}
|
|
)
|
|
|
|
def execute(self, context):
|
|
FilePath = self.filepath
|
|
filename, extension = os.path.splitext(self.filepath)
|
|
|
|
if extension != ".gpla":
|
|
extension = ".gpla"
|
|
FilePath = FilePath + ".gpla"
|
|
|
|
self.report({"INFO"}, FilePath)
|
|
|
|
if filename is not None and extension is not None:
|
|
fin = save_scene_to_file(bpy.context.scene, FilePath)
|
|
if fin == 0:
|
|
self.report({"INFO"}, "File write successful!")
|
|
return {"FINISHED"}
|
|
else:
|
|
self.report({"WARNING"}, "Something went wrong in saving the file")
|
|
else:
|
|
filename = None
|
|
extension = None
|
|
self.report({"WARNING"}, "The filename or extension isn't right, action stopped for your own safety")
|
|
return {"CANCELLED"}
|
|
|
|
|
|
class osci_render_close(bpy.types.Operator):
|
|
bl_label = "Close osci-render connection"
|
|
bl_idname = "render.osci_render_close"
|
|
|
|
def execute(self, context):
|
|
close_osci_render()
|
|
|
|
return {"FINISHED"}
|
|
|
|
|
|
@persistent
|
|
def close_osci_render():
|
|
global sock
|
|
if sock is not None:
|
|
try:
|
|
sock.send("CLOSE\n".encode('utf-8'))
|
|
sock.close()
|
|
except socket.error as exp:
|
|
sock = None
|
|
|
|
def get_gpla_file_allframes(scene):
|
|
bin = bytearray()
|
|
|
|
# header
|
|
bin.extend(("GPLA ").encode("utf8"))
|
|
bin.extend(GPLA_MAJOR.to_bytes(8, "little"))
|
|
bin.extend(GPLA_MINOR.to_bytes(8, "little"))
|
|
bin.extend(GPLA_PATCH.to_bytes(8, "little"))
|
|
|
|
# file info
|
|
bin.extend(("FILE ").encode("utf8"))
|
|
bin.extend(("fCount ").encode("utf8"))
|
|
bin.extend((scene.frame_end - scene.frame_start + 1).to_bytes(8, "little"))
|
|
bin.extend(("fRate ").encode("utf8"))
|
|
bin.extend(scene.render.fps.to_bytes(8, "little"))
|
|
bin.extend(("DONE ").encode("utf8"))
|
|
|
|
for frame in range(0, scene.frame_end - scene.frame_start + 1):
|
|
scene.frame_set(frame + scene.frame_start)
|
|
bin.extend(get_frame_info_binary())
|
|
|
|
bin.extend(("END GPLA").encode("utf8"))
|
|
|
|
return bin
|
|
|
|
def get_gpla_file(scene):
|
|
bin = bytearray()
|
|
|
|
# header
|
|
bin.extend(("GPLA ").encode("utf8"))
|
|
bin.extend(GPLA_MAJOR.to_bytes(8, "little"))
|
|
bin.extend(GPLA_MINOR.to_bytes(8, "little"))
|
|
bin.extend(GPLA_PATCH.to_bytes(8, "little"))
|
|
|
|
# file info
|
|
bin.extend(("FILE ").encode("utf8"))
|
|
bin.extend(("fCount ").encode("utf8"))
|
|
bin.extend((scene.frame_end - scene.frame_start + 1).to_bytes(8, "little"))
|
|
bin.extend(("fRate ").encode("utf8"))
|
|
bin.extend(scene.render.fps.to_bytes(8, "little"))
|
|
bin.extend(("DONE ").encode("utf8"))
|
|
|
|
bin.extend(get_frame_info_binary())
|
|
|
|
bin.extend(("END GPLA").encode("utf8"))
|
|
|
|
return bin
|
|
|
|
@persistent
|
|
def save_scene_to_file(scene, file_path):
|
|
return_frame = scene.frame_current
|
|
|
|
bin = get_gpla_file_allframes(scene)
|
|
|
|
if file_path is not None:
|
|
with open(file_path, "wb") as f:
|
|
f.write(bytes(bin))
|
|
else:
|
|
return 1
|
|
|
|
scene.frame_set(return_frame)
|
|
return 0
|
|
|
|
def get_frame_info_binary():
|
|
frame_info = bytearray()
|
|
frame_info.extend(("FRAME ").encode("utf8"))
|
|
|
|
frame_info.extend(("focalLen").encode("utf8"))
|
|
frame_info.extend(struct.pack("d", -0.05 * bpy.data.cameras[0].lens))
|
|
|
|
frame_info.extend(("OBJECTS ").encode("utf8"))
|
|
|
|
if (bpy.app.version[0] > 4) or (bpy.app.version[0] == 4 and bpy.app.version[1] >= 3):
|
|
for object in bpy.data.objects:
|
|
if object.visible_get() and object.type == 'GREASEPENCIL':
|
|
dg = bpy.context.evaluated_depsgraph_get()
|
|
obj = object.evaluated_get(dg)
|
|
frame_info.extend(("OBJECT ").encode("utf8"))
|
|
|
|
# matrix
|
|
frame_info.extend(("MATRIX ").encode("utf8"))
|
|
camera_space = bpy.context.scene.camera.matrix_world.inverted() @ obj.matrix_world
|
|
for i in range(4):
|
|
for j in range(4):
|
|
frame_info.extend(struct.pack("d", camera_space[i][j]))
|
|
frame_info.extend(("DONE ").encode("utf8"))
|
|
|
|
# strokes
|
|
frame_info.extend(("STROKES ").encode("utf8"))
|
|
layers = obj.data.layers
|
|
for layer in layers:
|
|
strokes = layer.frames.data.current_frame().drawing.strokes
|
|
for stroke in strokes:
|
|
frame_info.extend(("STROKE ").encode("utf8"))
|
|
|
|
frame_info.extend(("vertexCt").encode("utf8"))
|
|
frame_info.extend(len(stroke.points).to_bytes(8, "little"))
|
|
|
|
frame_info.extend(("VERTICES").encode("utf8"))
|
|
for vert in stroke.points:
|
|
frame_info.extend(struct.pack("d", vert.position.x))
|
|
frame_info.extend(struct.pack("d", vert.position.y))
|
|
frame_info.extend(struct.pack("d", vert.position.z))
|
|
# VERTICES
|
|
frame_info.extend(("DONE ").encode("utf8"))
|
|
|
|
# STROKE
|
|
frame_info.extend(("DONE ").encode("utf8"))
|
|
|
|
# STROKES
|
|
frame_info.extend(("DONE ").encode("utf8"))
|
|
|
|
# OBJECT
|
|
frame_info.extend(("DONE ").encode("utf8"))
|
|
else:
|
|
for object in bpy.data.objects:
|
|
if object.visible_get() and object.type == 'GPENCIL':
|
|
dg = bpy.context.evaluated_depsgraph_get()
|
|
obj = object.evaluated_get(dg)
|
|
frame_info.extend(("OBJECT ").encode("utf8"))
|
|
|
|
# matrix
|
|
frame_info.extend(("MATRIX ").encode("utf8"))
|
|
camera_space = bpy.context.scene.camera.matrix_world.inverted() @ obj.matrix_world
|
|
for i in range(4):
|
|
for j in range(4):
|
|
frame_info.extend(struct.pack("d", camera_space[i][j]))
|
|
# MATRIX
|
|
frame_info.extend(("DONE ").encode("utf8"))
|
|
|
|
# strokes
|
|
frame_info.extend(("STROKES ").encode("utf8"))
|
|
layers = obj.data.layers
|
|
for layer in layers:
|
|
strokes = layer.frames.data.active_frame.strokes
|
|
for stroke in strokes:
|
|
frame_info.extend(("STROKE ").encode("utf8"))
|
|
|
|
frame_info.extend(("vertexCt").encode("utf8"))
|
|
frame_info.extend(len(stroke.points).to_bytes(8, "little"))
|
|
|
|
frame_info.extend(("VERTICES").encode("utf8"))
|
|
for vert in stroke.points:
|
|
frame_info.extend(struct.pack("d", vert.co[0]))
|
|
frame_info.extend(struct.pack("d", vert.co[1]))
|
|
frame_info.extend(struct.pack("d", vert.co[2]))
|
|
# VERTICES
|
|
frame_info.extend(("DONE ").encode("utf8"))
|
|
|
|
# STROKE
|
|
frame_info.extend(("DONE ").encode("utf8"))
|
|
|
|
# STROKES
|
|
frame_info.extend(("DONE ").encode("utf8"))
|
|
|
|
# OBJECT
|
|
frame_info.extend(("DONE ").encode("utf8"))
|
|
|
|
# OBJECTS
|
|
frame_info.extend(("DONE ").encode("utf8"))
|
|
|
|
# FRAME
|
|
frame_info.extend(("DONE ").encode("utf8"))
|
|
|
|
return frame_info
|
|
|
|
|
|
@persistent
|
|
def send_scene_to_osci_render(scene):
|
|
global sock
|
|
|
|
if sock is not None:
|
|
bin = get_gpla_file(scene)
|
|
try:
|
|
sock.sendall(base64.b64encode(bytes(bin)) + "\n".encode("utf8"))
|
|
except socket.error as exp:
|
|
sock = None
|
|
|
|
|
|
operations = [OBJECT_PT_osci_render_settings, osci_render_connect, osci_render_close, osci_render_save]
|
|
|
|
|
|
def register():
|
|
bpy.types.Scene.oscirenderPort = bpy.props.IntProperty(name="osci-render port",description="The port through which osci-render will connect",min=51600,max=51699,default=51677)
|
|
bpy.app.handlers.frame_change_pre.append(send_scene_to_osci_render)
|
|
bpy.app.handlers.depsgraph_update_post.append(send_scene_to_osci_render)
|
|
atexit.register(close_osci_render)
|
|
for operation in operations:
|
|
bpy.utils.register_class(operation)
|
|
|
|
|
|
def unregister():
|
|
del bpy.types.Object.oscirenderPort
|
|
bpy.app.handlers.frame_change_pre.remove(send_scene_to_osci_render)
|
|
bpy.app.handlers.depsgraph_update_post.remove(send_scene_to_osci_render)
|
|
atexit.unregister(close_osci_render)
|
|
for operation in reversed(operations):
|
|
bpy.utils.unregister_class(operation)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
register()
|