├── .gitmodules ├── brte ├── __init__.py ├── converters │ ├── __init__.py │ └── btf.py ├── processors │ ├── __init__.py │ ├── double_buffer.py │ ├── dummy.py │ └── external_processor.py ├── converter_thread.py ├── processor_thread.py ├── socket_api.py └── engine.py ├── debug ├── addon.py └── __init__.py ├── README.md ├── .gitignore └── LICENSE /.gitmodules: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /brte/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /debug/addon.py: -------------------------------------------------------------------------------- 1 | if "bpy" in locals(): 2 | import imp 3 | imp.reload(engine) 4 | else: 5 | import bpy 6 | from brte import engine 7 | 8 | class DebugEngine(bpy.types.RenderEngine, engine.RealTimeEngine): 9 | bl_idname = 'RTE_DEBUG' 10 | bl_label = 'RTE Debug' 11 | -------------------------------------------------------------------------------- /brte/converters/__init__.py: -------------------------------------------------------------------------------- 1 | BTFConverter = None 2 | 3 | def alias(): 4 | global BTFConverter 5 | BTFConverter = btf.BTFConverter 6 | 7 | 8 | if '__imported__' in locals(): 9 | import imp 10 | imp.reload(btf) 11 | alias() 12 | else: 13 | __imported__ = True 14 | from . import btf 15 | alias() 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | BlenderRealtimeEngineAddon 2 | ========================== 3 | 4 | An addon to allow external real time engines (e.g. game engines) to use Blender as an editor and allow for closer integration. 5 | 6 | This addon does not do much on it's own. To see it in action with Panda3D, check out [BlenderPanda](https://github.com/Moguri/BlenderPanda) 7 | -------------------------------------------------------------------------------- /brte/processors/__init__.py: -------------------------------------------------------------------------------- 1 | DoubleBuffer = None 2 | DummyProcessor = None 3 | ExternalProcessor = None 4 | 5 | def alias(): 6 | global DoubleBuffer, DummyProcessor, ExternalProcessor 7 | DoubleBuffer = double_buffer.DoubleBuffer 8 | DummyProcessor = dummy.DummyProcessor 9 | ExternalProcessor = external_processor.ExternalProcessor 10 | 11 | if '__imported__' in locals(): 12 | import imp 13 | imp.reload(double_buffer) 14 | imp.reload(external_processor) 15 | imp.reload(dummy) 16 | alias() 17 | else: 18 | __imported__ = True 19 | from . import double_buffer 20 | from . import external_processor 21 | from . import dummy 22 | alias() 23 | -------------------------------------------------------------------------------- /brte/processors/double_buffer.py: -------------------------------------------------------------------------------- 1 | import ctypes 2 | 3 | 4 | class DoubleBuffer: 5 | def __init__(self, size, swap_callback): 6 | self.read_buffer = None 7 | self.write_buffer = None 8 | self.size = 0 9 | self.swap_callback = swap_callback 10 | 11 | self.resize(size) 12 | 13 | def resize(self, size): 14 | self.read_buffer = (ctypes.c_ubyte * size)(0) 15 | self.write_buffer = (ctypes.c_ubyte * size)(0) 16 | self.size = size 17 | 18 | def swap(self): 19 | self.read_buffer, self.write_buffer = self.write_buffer, self.read_buffer 20 | if self.swap_callback: 21 | self.swap_callback() 22 | -------------------------------------------------------------------------------- /brte/converter_thread.py: -------------------------------------------------------------------------------- 1 | import threading 2 | import queue 3 | 4 | 5 | class ConverterThread(threading.Thread): 6 | def __init__(self, converter, in_queue, out_queue, done_event): 7 | super().__init__() 8 | self.converter = converter 9 | self.in_queue = in_queue 10 | self.out_queue = out_queue 11 | self.done_event = done_event 12 | 13 | def run(self): 14 | while not self.done_event.is_set(): 15 | try: 16 | add, update, remove, view = self.in_queue.get(timeout=0.5) 17 | except queue.Empty: 18 | continue 19 | output = self.converter.convert(add, update, remove, view) 20 | self.in_queue.task_done() 21 | self.out_queue.put(output) 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .coverage.* 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | *,cover 45 | 46 | # Translations 47 | *.mo 48 | *.pot 49 | 50 | # Django stuff: 51 | *.log 52 | 53 | # Sphinx documentation 54 | docs/_build/ 55 | 56 | # PyBuilder 57 | target/ 58 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Daniel Stokes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /brte/processors/dummy.py: -------------------------------------------------------------------------------- 1 | '''An example module demonstrating the implementation of a processor''' 2 | import ctypes 3 | import math 4 | 5 | 6 | class DummyProcessor: 7 | '''An example processor that simply changes the viewport color''' 8 | 9 | def __init__(self): 10 | '''Construct a DummyProcessor with a buffer for drawing into''' 11 | 12 | self.value = 0 13 | self.buffer = (ctypes.c_ubyte * 3)(0) 14 | 15 | def process_data(self, data): 16 | '''Accept converted data to be consumed by the processor''' 17 | pass 18 | 19 | def destroy(self): 20 | '''Cleanup function called when the processor is no longer needed''' 21 | pass 22 | 23 | def update(self, timestep): 24 | '''Advance the processor by the timestep and update the viewport image''' 25 | 26 | self.value += timestep 27 | interval = 3.0 28 | 29 | while self.value > interval: 30 | self.value -= interval 31 | alpha = self.value / interval 32 | 33 | alpha = 1.0 - (0.5 * math.cos(2 * math.pi * alpha) + 0.5) 34 | ctypes.memset(self.buffer, int(255*alpha), 3) 35 | 36 | return ctypes.byref(self.buffer) 37 | -------------------------------------------------------------------------------- /debug/__init__.py: -------------------------------------------------------------------------------- 1 | bl_info = { 2 | "name": "RTE Debug", 3 | "author": "Daniel Stokes", 4 | "blender": (2, 75, 0), 5 | "location": "Info header, render engine menu", 6 | "description": "Debug implementation of the Realtime Engine Framework", 7 | "warning": "", 8 | "wiki_url": "", 9 | "tracker_url": "", 10 | "support": 'TESTING', 11 | "category": "Render"} 12 | 13 | if "bpy" in locals(): 14 | import imp 15 | imp.reload(addon) 16 | else: 17 | import bpy 18 | from .addon import DebugEngine 19 | 20 | 21 | def register(): 22 | panels = [getattr(bpy.types, t) for t in dir(bpy.types) if 'PT' in t] 23 | for panel in panels: 24 | if hasattr(panel, 'COMPAT_ENGINES') and 'BLENDER_GAME' in panel.COMPAT_ENGINES: 25 | panel.COMPAT_ENGINES.add('RTE_DEBUG') 26 | bpy.utils.register_module(__name__) 27 | 28 | 29 | def unregister(): 30 | panels = [getattr(bpy.types, t) for t in dir(bpy.types) if 'PT' in t] 31 | for panel in panels: 32 | if hasattr(panel, 'COMPAT_ENGINES') and 'RTE_FRAMEWORK' in panel.COMPAT_ENGINES: 33 | panel.COMPAT_ENGINES.remove('RTE_DEBUG') 34 | bpy.utils.unregister_module(__name__) 35 | -------------------------------------------------------------------------------- /brte/processor_thread.py: -------------------------------------------------------------------------------- 1 | import threading 2 | import time 3 | import queue 4 | 5 | 6 | class ProcessorThread(threading.Thread): 7 | def __init__(self, processor, data_queue, update_queue, out_queue, done_event): 8 | super().__init__() 9 | self.processor = processor 10 | self.data_queue = data_queue 11 | self.update_queue = update_queue 12 | self.out_queue = out_queue 13 | self.done_event = done_event 14 | 15 | def run(self): 16 | while not self.done_event.is_set(): 17 | try: 18 | data = self.data_queue.get_nowait() 19 | self.processor.process_data(data) 20 | self.data_queue.task_done() 21 | except queue.Empty: 22 | pass 23 | 24 | try: 25 | # Coalesce updates if we are getting behind 26 | pending_updates = self.update_queue.qsize() 27 | if pending_updates > 10: 28 | dt = 0 29 | for i in range(pending_updates - 3): 30 | dt += self.update_queue.get() 31 | self.update_queue.task_done() 32 | else: 33 | dt = self.update_queue.get_nowait() 34 | self.update_queue.task_done() 35 | image_ref = self.processor.update(dt) 36 | if image_ref is not None: 37 | self.out_queue.put(image_ref) 38 | except queue.Empty: 39 | pass 40 | 41 | time.sleep(0.001) 42 | 43 | self.processor.destroy() 44 | -------------------------------------------------------------------------------- /brte/converters/btf.py: -------------------------------------------------------------------------------- 1 | if 'imported' in locals(): 2 | import imp 3 | import bpy 4 | imp.reload(blendergltf) 5 | else: 6 | imported = True 7 | import blendergltf 8 | 9 | 10 | import json 11 | import math 12 | 13 | import bpy 14 | 15 | 16 | def togl(matrix): 17 | return [i for col in matrix.col for i in col] 18 | 19 | 20 | class BTFConverter: 21 | def __init__(self, gltf_settings=None): 22 | if gltf_settings is None: 23 | available_extensions = blendergltf.extension_exporters 24 | gltf_settings = { 25 | 'images_data_storage': 'REFERENCE', 26 | 'nodes_export_hidden': True, 27 | 'images_allow_srgb': True, 28 | 'asset_profile': 'DESKTOP', 29 | 'asset_version': '1.0', 30 | 'hacks_streaming': True, 31 | 'meshes_apply_modifiers': False, # Cannot be done in a thread 32 | 'extension_exporters': [ 33 | available_extensions.khr_materials_common.KhrMaterialsCommon(), 34 | available_extensions.blender_physics.BlenderPhysics(), 35 | ], 36 | } 37 | 38 | self.gltf_settings = gltf_settings 39 | 40 | def convert(self, add_delta, update_delta, remove_delta, view_delta): 41 | for key, value in update_delta.items(): 42 | if value: 43 | add_delta[key] = value 44 | 45 | data = blendergltf.export_gltf(add_delta, self.gltf_settings) 46 | 47 | if view_delta: 48 | self.export_view(view_delta, data) 49 | 50 | return data 51 | 52 | def export_view(self, view_delta, gltf): 53 | if 'extras' not in gltf: 54 | gltf['extras'] = {} 55 | gltf['extras']['view'] = {} 56 | 57 | if 'viewport' in view_delta: 58 | gltf['extras']['view'] = { 59 | 'width' : view_delta['viewport'].width, 60 | 'height' : view_delta['viewport'].height, 61 | } 62 | if 'projection_matrix' in view_delta: 63 | gltf['extras']['view']['projection_matrix'] = togl(view_delta['projection_matrix']) 64 | if 'view_matrix' in view_delta: 65 | gltf['extras']['view']['view_matrix'] = togl(view_delta['view_matrix']) 66 | -------------------------------------------------------------------------------- /brte/processors/external_processor.py: -------------------------------------------------------------------------------- 1 | import ctypes 2 | import json 3 | import socket 4 | import struct 5 | import subprocess 6 | import time 7 | 8 | 9 | MSG_DATA = 0 10 | MSG_UPDATE = 1 11 | 12 | 13 | class ExternalProcessor: 14 | def __init__(self, command, port=5555): 15 | self.width = 1 16 | self.height = 1 17 | self.buffer = (ctypes.c_ubyte * 3)(0) 18 | self.value = 0 19 | self.is_connected = False 20 | 21 | self.listen_socket = socket.socket() 22 | self.listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 23 | self.listen_socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) 24 | self.listen_socket.bind(('localhost', port)) 25 | self.listen_socket.listen(20) 26 | self.listen_socket.settimeout(5) 27 | 28 | self.process = subprocess.Popen(command) 29 | 30 | def destroy(self): 31 | if hasattr(self, "socket"): 32 | try: 33 | self.socket.shutdown(socket.SHUT_RDWR) 34 | except OSError: 35 | pass 36 | self.socket.close() 37 | try: 38 | self.listen_socket.shutdown(socket.SHUT_RDWR) 39 | except OSError: 40 | pass 41 | self.listen_socket.close() 42 | 43 | def _connect(self): 44 | if not self.is_connected: 45 | try: 46 | self.socket = self.listen_socket.accept()[0] 47 | self.is_connected = True 48 | print('Connected to external process') 49 | except: 50 | print('Unable to connect to external process') 51 | 52 | def process_data(self, data): 53 | '''Accept converted data to be consumed by the processor''' 54 | self._connect() 55 | if not self.is_connected: 56 | return 57 | 58 | payload = json.dumps(data, check_circular=False).encode('ascii') 59 | 60 | self.socket.sendall(struct.pack('=HI', MSG_DATA, len(payload))) 61 | self.socket.sendall(payload) 62 | self.socket.recv(1) 63 | 64 | def update(self, timestep): 65 | '''Advance the processor by the timestep and update the viewport image''' 66 | self._connect() 67 | if not self.is_connected: 68 | return None 69 | 70 | self.socket.sendall(struct.pack('=Hf', MSG_UPDATE, timestep)) 71 | 72 | start = time.perf_counter() 73 | width, height = struct.unpack('=HH', self.socket.recv(4)) 74 | if width != self.width or height != self.height: 75 | self.width = width 76 | self.height = height 77 | self.buffer = (ctypes.c_ubyte * (self.width * self.height * 3))() 78 | 79 | data_size = self.width*self.height*3 80 | remaining = data_size 81 | view = memoryview(self.buffer) 82 | while remaining > 0: 83 | rcv_size = self.socket.recv_into(view, remaining) 84 | view = view[rcv_size:] 85 | remaining -= rcv_size 86 | 87 | transfer_t = time.perf_counter() - start 88 | # print('Blender: Update time: {}ms'.format(transfer_t * 1000)) 89 | # print('Blender: Speed: {} Gbit/s'.format(data_size/1024/1024/1024*8 / transfer_t)) 90 | 91 | return self.width, self.height, ctypes.byref(self.buffer) 92 | -------------------------------------------------------------------------------- /brte/socket_api.py: -------------------------------------------------------------------------------- 1 | import enum 2 | import json 3 | import socket 4 | import struct 5 | import sys 6 | import time 7 | 8 | 9 | class AutoNumber(enum.Enum): 10 | def __new__(cls): 11 | value = len(cls.__members__) + 1 12 | obj = object.__new__(cls) 13 | obj._value_ = value 14 | return obj 15 | 16 | 17 | class MethodIDs(AutoNumber): 18 | __order__ = "add update remove" 19 | add = () 20 | update = () 21 | remove = () 22 | 23 | 24 | class DataIDs(AutoNumber): 25 | __order__ = "view projection viewport gltf" 26 | view = () 27 | projection = () 28 | viewport = () 29 | gltf = () 30 | 31 | 32 | def send_message(_socket, method, data_id, data): 33 | _socket.setblocking(True) 34 | _socket.settimeout(1) 35 | 36 | attempts = 3 37 | while attempts > 0: 38 | message = encode_cmd_message(method, data_id) 39 | try: 40 | _socket.send(message) 41 | data_str = json.dumps(data) 42 | _socket.send(struct.pack("I", len(data_str))) 43 | _socket.send(data_str.encode()) 44 | except socket.timeout: 45 | attempts -= 1 46 | continue 47 | 48 | break 49 | else: 50 | print("Failed to send message (%s, %s)." % (method.name, data_id.name)) 51 | 52 | _socket.setblocking(False) 53 | 54 | 55 | def encode_cmd_message(method_id, data_id): 56 | message = data_id.value & 0b00001111 57 | message |= method_id.value << 4 58 | return struct.pack("B", message) 59 | 60 | 61 | def decode_cmd_message(message): 62 | message = struct.unpack('B', message)[0] 63 | method_id = message >> 4 64 | data_id = message & 0b00001111 65 | return MethodIDs(method_id), DataIDs(data_id) 66 | 67 | 68 | def decode_size_message(message): 69 | return struct.unpack('I', message)[0] 70 | 71 | 72 | class SocketClient(object): 73 | def __init__(self, handler): 74 | self.handler = handler 75 | 76 | self.socket = socket.socket() 77 | self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 78 | self.socket.connect(("127.0.0.1", 4242)) 79 | 80 | def run(self): 81 | try: 82 | while True: 83 | try: 84 | self.socket.setblocking(False) 85 | message = self.socket.recv(1) 86 | if message: 87 | self.socket.setblocking(True) 88 | method_id, data_id = decode_cmd_message(message) 89 | size = decode_size_message(self.socket.recv(4)) 90 | 91 | data = b"" 92 | remaining = size 93 | while remaining > 0: 94 | chunk = self.socket.recv(min(1024, remaining)) 95 | remaining -= len(chunk) 96 | data += chunk 97 | data = json.loads(data.decode()) 98 | if data_id == DataIDs.projection: 99 | if hasattr(self.handler, 'handle_projection'): 100 | self.handler.handle_projection(data['data']) 101 | elif data_id == DataIDs.view: 102 | if hasattr(self.handler, 'handle_view'): 103 | self.handler.handle_view(data['data']) 104 | elif data_id == DataIDs.viewport: 105 | if hasattr(self.handler, 'handle_viewport'): 106 | self.handler.handle_viewport(data['width'], data['height']) 107 | elif data_id == DataIDs.gltf: 108 | if hasattr(self.handler, 'handle_gltf'): 109 | self.handler.handle_gltf(data) 110 | except socket.error: 111 | break 112 | 113 | # Return output 114 | img_data, sx, sy = self.handler.get_render_image() 115 | data_size = len(img_data) 116 | self.socket.setblocking(True) 117 | try: 118 | self.socket.send(struct.pack("HH", sx, sy)) 119 | 120 | start_time = time.clock() 121 | sent_count = 0 122 | 123 | while sent_count < data_size: 124 | sent_count += self.socket.send(img_data[sent_count:data_size - sent_count]) 125 | etime = time.clock() - start_time 126 | tx = 0 if etime == 0 else sent_count*8/1024/1024/etime 127 | #print("Sent %d bytes in %.2f ms (%.2f Mbps)" % (sent_count, etime*1000, tx)) 128 | except socket.timeout: 129 | print("Failed to send result data") 130 | except socket.error as e: 131 | print("Closing") 132 | self.socket.close() 133 | sys.exit() 134 | -------------------------------------------------------------------------------- /brte/engine.py: -------------------------------------------------------------------------------- 1 | if "bpy" in locals(): 2 | import imp 3 | imp.reload(socket_api) 4 | imp.reload(_converters) 5 | imp.reload(processors) 6 | imp.reload(converter_thread) 7 | imp.reload(processor_thread) 8 | else: 9 | import bpy 10 | from . import socket_api 11 | from . import converters as _converters 12 | from . import processors 13 | from . import converter_thread 14 | from . import processor_thread 15 | 16 | import os 17 | import socket 18 | import struct 19 | import subprocess 20 | import sys 21 | import time 22 | import threading 23 | import collections 24 | import queue 25 | 26 | import bpy 27 | import mathutils 28 | 29 | from OpenGL.GL import * 30 | 31 | 32 | DEFAULT_WATCHLIST = [ 33 | #"actions", 34 | #"armatures", 35 | "cameras", 36 | "images", 37 | "lamps", 38 | "materials", 39 | "meshes", 40 | "objects", 41 | "scenes", 42 | #"sounds", 43 | #"speakers", 44 | "textures", 45 | #"worlds", 46 | ] 47 | 48 | 49 | ViewportTuple = collections.namedtuple('Viewport', ('height', 'width')) 50 | 51 | 52 | # Currently need some things stored globally so they can be cleaned up after 53 | # losing the RealTimeEngine reference 54 | class G: 55 | done_event = None 56 | thread_converter = None 57 | thread_processor = None 58 | 59 | def cleanup_threads(): 60 | if threading.get_ident() != threading.main_thread().ident: 61 | # Only let the main thread cleanup threads 62 | return 63 | 64 | if G.done_event is not None: 65 | G.done_event.set() 66 | if G.thread_converter is not None: 67 | G.thread_converter.join() 68 | G.thread_converter = None 69 | if G.thread_processor is not None: 70 | G.thread_processor.join() 71 | G.thread_processor = None 72 | 73 | 74 | def get_collection_name(collection): 75 | class_name = collection.rna_type.__class__.__name__ 76 | clean_name = class_name.replace("BlendData", "").lower() 77 | return clean_name 78 | 79 | 80 | class RealTimeEngine(): 81 | bl_idname = 'RTE_FRAMEWORK' 82 | bl_label = "Real Time Engine Framework" 83 | 84 | def __init__(self, **kwargs): 85 | # Display image 86 | self.clock = time.perf_counter() 87 | 88 | self.queue_pre_convert = queue.Queue() 89 | self.queue_post_convert = queue.Queue() 90 | self.queue_update = queue.Queue() 91 | self.queue_image = queue.Queue() 92 | 93 | G.cleanup_threads() 94 | 95 | G.done_event = threading.Event() 96 | 97 | converter = kwargs.get('converter', _converters.BTFConverter()) 98 | G.thread_converter = converter_thread.ConverterThread(converter, 99 | self.queue_pre_convert, self.queue_post_convert, G.done_event) 100 | G.thread_converter.start() 101 | 102 | processor = kwargs.get('processor', processors.DummyProcessor()) 103 | G.thread_processor = processor_thread.ProcessorThread(processor, 104 | self.queue_post_convert, self.queue_update, self.queue_image, 105 | G.done_event) 106 | G.thread_processor.start() 107 | 108 | self.use_bgr_texture = kwargs.get('use_bgr_texture', False) 109 | 110 | self.remove_delta = {} 111 | self.add_delta = {} 112 | self.update_delta = {} 113 | self.view_delta = {} 114 | 115 | watch_list = kwargs['watch_list'] if 'watch_list' in kwargs else DEFAULT_WATCHLIST 116 | self._watch_list = [getattr(bpy.data, i) for i in watch_list] 117 | 118 | self._tracking_sets = {} 119 | for collection in self._watch_list: 120 | collection_name = get_collection_name(collection) 121 | self._tracking_sets[collection_name] = set() 122 | 123 | self._old_vmat = None 124 | self._old_pmat = None 125 | self._old_viewport = None 126 | 127 | def main_loop(scene): 128 | if threading.get_ident() != threading.main_thread().ident: 129 | print("Wrong thread", threading.current_thread()) 130 | import inspect 131 | for i in inspect.stack(): 132 | print(str(i)) 133 | return 134 | 135 | try: 136 | new_time = time.perf_counter() 137 | dt = new_time - self.clock 138 | self.clock = new_time 139 | self.main_update(dt) 140 | except ReferenceError: 141 | bpy.app.handlers.scene_update_post.remove(main_loop) 142 | G.cleanup_threads() 143 | 144 | bpy.app.handlers.scene_update_post.append(main_loop) 145 | 146 | self.tex = glGenTextures(1) 147 | glBindTexture(GL_TEXTURE_2D, self.tex) 148 | glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB8, 1, 1, 0, 149 | GL_RGB, GL_UNSIGNED_BYTE, struct.pack('=BBB', 0, 0, 0)) 150 | 151 | def __del__(self): 152 | G.cleanup_threads() 153 | 154 | @classmethod 155 | def register(cls): 156 | render_engine_class = cls 157 | class LaunchGame(bpy.types.Operator): 158 | '''Launch the game in a separate window''' 159 | bl_idname = '{}.launch_game'.format(cls.bl_idname.lower()) 160 | bl_label = 'Launch Game' 161 | 162 | @classmethod 163 | def poll(cls, context): 164 | return context.scene.render.engine == render_engine_class.bl_idname 165 | 166 | def execute(self, context): 167 | try: 168 | cls.launch_game() 169 | except: 170 | self.report({'ERROR'}, str(sys.exc_info()[1])) 171 | return {'FINISHED'} 172 | 173 | bpy.utils.register_class(LaunchGame) 174 | if not bpy.app.background: 175 | km = bpy.context.window_manager.keyconfigs.default.keymaps['Screen'] 176 | ki = km.keymap_items.new(LaunchGame.bl_idname, 'P', 'PRESS') 177 | 178 | def view_update(self, context): 179 | """ Called when the scene is changed """ 180 | 181 | for collection in self._watch_list: 182 | collection_name = get_collection_name(collection) 183 | collection_set = set(collection) 184 | tracking_set = self._tracking_sets[collection_name] 185 | 186 | # Check for new items 187 | add_set = collection_set - tracking_set 188 | self.add_delta[collection_name] = add_set 189 | tracking_set |= add_set 190 | 191 | # Check for removed items 192 | remove_set = tracking_set - collection_set 193 | self.remove_delta[collection_name] = remove_set 194 | tracking_set -= remove_set 195 | 196 | # Check for updates 197 | update_set = {item for item in collection if item.is_updated} 198 | self.update_delta[collection_name] = update_set 199 | 200 | def view_draw(self, context): 201 | """ Called when viewport settings change """ 202 | region = context.region 203 | view = context.region_data 204 | 205 | vmat = view.view_matrix.copy() 206 | vmat_inv = vmat.inverted() 207 | pmat = view.perspective_matrix * vmat_inv 208 | 209 | viewport = [region.x, region.y, region.width, region.height] 210 | 211 | self.update_view(vmat, pmat, viewport) 212 | 213 | 214 | glPushAttrib(GL_ALL_ATTRIB_BITS) 215 | glActiveTexture(GL_TEXTURE0) 216 | glBindTexture(GL_TEXTURE_2D, self.tex) 217 | try: 218 | image_ref = self.queue_image.get_nowait() 219 | image_format = GL_BGR if self.use_bgr_texture else GL_RGB 220 | glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB8, image_ref[0], image_ref[1], 0, image_format, 221 | GL_UNSIGNED_BYTE, image_ref[2]) 222 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST) 223 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST) 224 | self.queue_image.task_done() 225 | except queue.Empty: 226 | pass 227 | 228 | glDisable(GL_DEPTH_TEST) 229 | glDisable(GL_CULL_FACE) 230 | glDisable(GL_STENCIL_TEST) 231 | glEnable(GL_TEXTURE_2D) 232 | 233 | glClearColor(0, 0, 1, 1) 234 | glClear(GL_COLOR_BUFFER_BIT) 235 | 236 | glMatrixMode(GL_MODELVIEW) 237 | glPushMatrix() 238 | glLoadIdentity() 239 | glMatrixMode(GL_PROJECTION) 240 | glPushMatrix() 241 | glLoadIdentity() 242 | 243 | glBegin(GL_QUADS) 244 | glColor3f(1.0, 1.0, 1.0) 245 | glTexCoord2f(0.0, 0.0) 246 | glVertex3i(-1, -1, 0) 247 | glTexCoord2f(1.0, 0.0) 248 | glVertex3i(1, -1, 0) 249 | glTexCoord2f(1.0, 1.0) 250 | glVertex3i(1, 1, 0) 251 | glTexCoord2f(0.0, 1.0) 252 | glVertex3i(-1, 1, 0) 253 | glEnd() 254 | 255 | glPopMatrix() 256 | glMatrixMode(GL_MODELVIEW) 257 | glPopMatrix() 258 | 259 | glPopAttrib() 260 | 261 | def update_view(self, view_matrix, projection_matrix, viewport): 262 | if view_matrix != self._old_vmat: 263 | self._old_vmat = view_matrix 264 | self.view_delta['view_matrix'] = view_matrix 265 | 266 | if projection_matrix != self._old_pmat: 267 | self._old_pmat = projection_matrix 268 | self.view_delta['projection_matrix'] = projection_matrix 269 | 270 | if viewport != self._old_viewport: 271 | self._old_viewport = viewport 272 | self.view_delta['viewport'] = ViewportTuple(width=viewport[2], height=viewport[3]) 273 | 274 | def draw_callback(self): 275 | '''Forces a view_draw to occur''' 276 | self.tag_redraw() 277 | 278 | def main_update(self, dt): 279 | if self.add_delta or self.update_delta or self.view_delta: 280 | self.queue_pre_convert.put(( 281 | self.add_delta.copy(), 282 | self.update_delta.copy(), 283 | self.remove_delta.copy(), 284 | self.view_delta.copy(), 285 | )) 286 | 287 | self.add_delta.clear() 288 | self.update_delta.clear() 289 | self.remove_delta.clear() 290 | self.view_delta.clear() 291 | 292 | self.queue_update.put(dt) 293 | 294 | self.draw_callback() 295 | 296 | # Useful debug information for checking if a queue is filling up 297 | # print('Approximate queue sizes:') 298 | # print('\tPre Convert:', self.queue_pre_convert.qsize()) 299 | # print('\tPost Convert:', self.queue_post_convert.qsize()) 300 | # print('\tUpdate:', self.queue_update.qsize()) 301 | # print('\tImage:', self.queue_image.qsize()) 302 | 303 | @classmethod 304 | def launch_game(cls): 305 | pass 306 | --------------------------------------------------------------------------------