''' Copyright (C) 2023 CG Cookie http://cgcookie.com hello@cgcookie.com Created by Jonathan Denning, Jonathan Williamson This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . ''' import sys import math import copy import json import time import random from itertools import chain from queue import Queue from concurrent.futures import ThreadPoolExecutor import bpy import gpu import bmesh from bmesh.types import BMesh, BMVert, BMEdge, BMFace from mathutils import Matrix, Vector from mathutils.bvhtree import BVHTree from mathutils.kdtree import KDTree from mathutils.geometry import normal as compute_normal, intersect_point_tri from ...addon_common.common import gpustate from ...addon_common.common import bmesh_render as bmegl from ...addon_common.common.blender import tag_redraw_all from ...addon_common.common.bmesh_render import triangulateFace, BufferedRender_Batch from ...addon_common.common.debug import dprint, Debugger from ...addon_common.common.decorators import stats_wrapper from ...addon_common.common.globals import Globals from ...addon_common.common.hasher import hash_object, hash_bmesh from ...addon_common.common.profiler import profiler from ...addon_common.common.maths import ( Point, Direction, Normal, Frame, Point2D, Vec2D, Direction2D, Ray, XForm, BBox, Plane, ) from ...addon_common.common.utils import min_index from ...config.options import options from .rfmesh_wrapper import ( BMElemWrapper, RFVert, RFEdge, RFFace, RFEdgeSequence, ) class RFMeshRender(): ''' RFMeshRender handles rendering RFMeshes. ''' cache = {} create_count = 0 delete_count = 0 @staticmethod # @profiler.function def new(rfmesh, opts, always_dirty=False): # TODO: REIMPLEMENT CACHING!! # HAD TO DISABLE THIS BECAUSE 2.83 AND 2.90 WOULD CRASH # WHEN RESTARTING RF. PROBABLY DUE TO HOLDING REFS TO # OLD DATA (CRASH DUE TO FREEING INVALID DATA??) if False: if True: # with profiler.code('hashing object'): ho = hash_object(rfmesh.obj) if True: # with profiler.code('hashing bmesh'): hb = hash_bmesh(rfmesh.bme) h = (ho, hb) if h not in RFMeshRender.cache: RFMeshRender.creating = True RFMeshRender.cache[h] = RFMeshRender(rfmesh, opts) del RFMeshRender.creating rfmrender = RFMeshRender.cache[h] else: RFMeshRender.creating = True rfmrender = RFMeshRender(rfmesh, opts) del RFMeshRender.creating rfmrender.always_dirty = always_dirty return rfmrender # @profiler.function def __init__(self, rfmesh, opts): assert hasattr(RFMeshRender, 'creating'), ( 'Do not create new RFMeshRender directly!' 'Use RFMeshRender.new()') RFMeshRender.create_count += 1 # print('RFMeshRender.__init__', RFMeshRender.create_count, RFMeshRender.delete_count) # initially loading asynchronously? self.async_load = options['async mesh loading'] self._is_loading = False self._is_loaded = False self.load_verts = opts.get('load verts', True) self.load_edges = opts.get('load edges', True) self.load_faces = opts.get('load faces', True) self.buf_data_queue = Queue() self.buf_matrix_model = rfmesh.xform.to_gpubuffer_Model() self.buf_matrix_inverse = rfmesh.xform.to_gpubuffer_Inverse() self.buf_matrix_normal = rfmesh.xform.to_gpubuffer_Normal() self.buffered_renders_static = [] self.buffered_renders_dynamic = [] self.split = None self.drawing = Globals.drawing self.opts = {} self.replace_rfmesh(rfmesh) self.replace_opts(opts) def __del__(self): RFMeshRender.delete_count += 1 # print('RFMeshRender.__del__', self.rfmesh, RFMeshRender.create_count, RFMeshRender.delete_count) self.bmesh.free() if hasattr(self, 'buf_matrix_model'): del self.buf_matrix_model if hasattr(self, 'buf_matrix_inverse'): del self.buf_matrix_inverse if hasattr(self, 'buf_matrix_normal'): del self.buf_matrix_normal if hasattr(self, 'buffered_renders_static'): del self.buffered_renders_static if hasattr(self, 'buffered_renders_dynamic'): del self.buffered_renders_dynamic if hasattr(self, 'bmesh'): del self.bmesh if hasattr(self, 'rfmesh'): del self.rfmesh # @profiler.function def replace_opts(self, opts): opts = dict(opts) opts['dpi mult'] = self.drawing.get_dpi_mult() if opts == self.opts: return self.opts = opts self.rfmesh_version = None # @profiler.function def replace_rfmesh(self, rfmesh): self.rfmesh = rfmesh self.bmesh = rfmesh.bme self.rfmesh_version = None def dirty(self): self.rfmesh_version = None # @profiler.function def add_buffered_render(self, draw_type, data, static): batch = BufferedRender_Batch(draw_type) batch.buffer(data['vco'], data['vno'], data['sel'], data['warn'], data['pin'], data['seam']) if static: self.buffered_renders_static.append(batch) else: self.buffered_renders_dynamic.append(batch) def split_visualization(self, verts=None, edges=None, faces=None): if not verts and not edges and not faces: self.split = None else: unwrap = BMElemWrapper._unwrap verts = { unwrap(v) for v in verts } if verts else set() edges = { unwrap(e) for e in edges } if edges else set() faces = { unwrap(f) for f in faces } if faces else set() edges.update(e for v in verts for e in v.link_edges) faces.update(f for e in edges for f in e.link_faces) verts.update(v for e in edges for v in e.verts) verts.update(v for f in faces for v in f.verts) edges.update(e for f in faces for e in f.edges) self.split = { 'gathered static': False, 'static verts': { v for v in self.bmesh.verts if v not in verts }, 'static edges': { e for e in self.bmesh.edges if e not in edges }, 'static faces': { f for f in self.bmesh.faces if f not in faces }, 'gathered dynamic': False, 'dynamic verts': verts, 'dynamic edges': edges, 'dynamic faces': faces, } self.dirty() # @profiler.function def _gather_data(self): if not self.split: self.buffered_renders_static = [] self.buffered_renders_dynamic = [] else: if not self.split['gathered dynamic']: self.buffered_renders_static = [] self.split['gathered dynamic'] = True self.buffered_renders_dynamic = [] mirror_axes = self.rfmesh.mirror_mod.xyz if self.rfmesh.mirror_mod else [] mirror_x = 'x' in mirror_axes mirror_y = 'y' in mirror_axes mirror_z = 'z' in mirror_axes layer_pin = self.rfmesh.layer_pin def gather(verts, edges, faces, static): vert_count = 100_000 edge_count = 50_000 face_count = 10_000 ''' IMPORTANT NOTE: DO NOT USE PROFILER INSIDE THIS FUNCTION IF LOADING ASYNCHRONOUSLY! ''' def sel(g): return 1.0 if g.select else 0.0 def warn_vert(g): if mirror_x and g.co.x <= 0.0001: return 0.0 if mirror_y and g.co.y >= -0.0001: return 0.0 if mirror_z and g.co.z <= 0.0001: return 0.0 return 0.0 if g.is_manifold and not g.is_boundary else 1.0 def warn_edge(g): v0,v1 = g.verts if mirror_x and v0.co.x <= 0.0001 and v1.co.x <= 0.0001: return 0.0 if mirror_y and v0.co.y >= -0.0001 and v1.co.y >= -0.0001: return 0.0 if mirror_z and v0.co.z <= 0.0001 and v1.co.z <= 0.0001: return 0.0 return 0.0 if g.is_manifold else 1.0 def warn_face(g): return 1.0 def pin_vert(g): if not layer_pin: return 0.0 return 1.0 if g[layer_pin] else 0.0 def pin_edge(g): return 1.0 if all(pin_vert(v) for v in g.verts) else 0.0 def pin_face(g): return 1.0 if all(pin_vert(v) for v in g.verts) else 0.0 def seam_vert(g): return 1.0 if any(e.seam for e in g.link_edges) else 0.0 def seam_edge(g): return 1.0 if g.seam else 0.0 def seam_face(g): return 0.0 try: time_start = time.time() # NOTE: duplicating data rather than using indexing, otherwise # selection will bleed if True: # with profiler.code('gathering', enabled=not self.async_load): if self.load_faces: tri_faces = [(bmf, list(bmvs)) for bmf in faces if bmf.is_valid and not bmf.hide for bmvs in triangulateFace(bmf.verts) ] l = len(tri_faces) for i0 in range(0, l, face_count): i1 = min(l, i0 + face_count) face_data = { 'vco': [ tuple(bmv.co) for bmf, verts in tri_faces[i0:i1] for bmv in verts ], 'vno': [ tuple(bmv.normal) for bmf, verts in tri_faces[i0:i1] for bmv in verts ], 'sel': [ sel(bmf) for bmf, verts in tri_faces[i0:i1] for _ in verts ], 'warn': [ warn_face(bmf) for bmf, verts in tri_faces[i0:i1] for _ in verts ], 'pin': [ pin_face(bmf) for bmf, verts in tri_faces[i0:i1] for _ in verts ], 'seam': [ seam_face(bmf) for bmf, verts in tri_faces[i0:i1] for _ in verts ], 'idx': None, # list(range(len(tri_faces)*3)), } if self.async_load: self.buf_data_queue.put((BufferedRender_Batch.TRIANGLES, face_data, static)) tag_redraw_all('buffer update') else: self.add_buffered_render(BufferedRender_Batch.TRIANGLES, face_data, static) if self.load_edges: edges = [bme for bme in edges if bme.is_valid and not bme.hide] l = len(edges) for i0 in range(0, l, edge_count): i1 = min(l, i0 + edge_count) edge_data = { 'vco': [ tuple(bmv.co) for bme in edges[i0:i1] for bmv in bme.verts ], 'vno': [ tuple(bmv.normal) for bme in edges[i0:i1] for bmv in bme.verts ], 'sel': [ sel(bme) for bme in edges[i0:i1] for _ in bme.verts ], 'warn': [ warn_edge(bme) for bme in edges[i0:i1] for _ in bme.verts ], 'pin': [ pin_edge(bme) for bme in edges[i0:i1] for _ in bme.verts ], 'seam': [ seam_edge(bme) for bme in edges[i0:i1] for _ in bme.verts ], 'idx': None, # list(range(len(self.bmesh.edges)*2)), } if self.async_load: self.buf_data_queue.put((BufferedRender_Batch.LINES, edge_data, static)) tag_redraw_all('buffer update') else: self.add_buffered_render(BufferedRender_Batch.LINES, edge_data, static) if self.load_verts: verts = [bmv for bmv in verts if bmv.is_valid and not bmv.hide] l = len(verts) for i0 in range(0, l, vert_count): i1 = min(l, i0 + vert_count) vert_data = { 'vco': [ tuple(bmv.co) for bmv in verts[i0:i1] ], 'vno': [ tuple(bmv.normal) for bmv in verts[i0:i1] ], 'sel': [ sel(bmv) for bmv in verts[i0:i1] ], 'warn': [ warn_vert(bmv) for bmv in verts[i0:i1] ], 'pin': [ pin_vert(bmv) for bmv in verts[i0:i1] ], 'seam': [ seam_vert(bmv) for bmv in verts[i0:i1] ], 'idx': None, # list(range(len(self.bmesh.verts))), } if self.async_load: self.buf_data_queue.put((BufferedRender_Batch.POINTS, vert_data, static)) tag_redraw_all('buffer update') else: self.add_buffered_render(BufferedRender_Batch.POINTS, vert_data, static) if self.async_load: self.buf_data_queue.put('done') time_end = time.time() # print('RFMeshRender: Gather time: %0.2f' % (time_end - time_start)) except Exception as e: print('EXCEPTION WHILE GATHERING: ' + str(e)) raise e # self.bmesh.verts.ensure_lookup_table() for bmv in self.bmesh.verts: if bmv.link_faces: bmv.normal_update() # for bmelem in chain(self.bmesh.faces, self.bmesh.edges): # bmelem.normal_update() self._is_loading = True self._is_loaded = False # with profiler.code('Gathering data for RFMesh (%ssync)' % ('a' if self.async_load else '')): if not self.async_load: #print(f'RFMeshRender._gather: synchronous') #profiler.function(gather)() if not self.split: #print(f' v={len(self.bmesh.verts)} e={len(self.bmesh.edges)} f={len(self.bmesh.faces)}') gather(self.bmesh.verts, self.bmesh.edges, self.bmesh.faces, True) else: if not self.split['gathered static']: #print(f' sv={len(self.split["static verts"])} se={len(self.split["static edges"])} sf={len(self.split["static faces"])}') gather(self.split['static verts'], self.split['static edges'], self.split['static faces'], True) self.split['gathered static'] = True #print(f' dv={len(self.split["dynamic verts"])} de={len(self.split["dynamic edges"])} df={len(self.split["dynamic faces"])}') gather(self.split['dynamic verts'], self.split['dynamic edges'], self.split['dynamic faces'], False) else: #print(f'RFMeshRender._gather: asynchronous') #self._gather_submit = ThreadPoolExecutor.submit(gather) e = ThreadPoolExecutor() if not self.split: #print(f' v={len(self.bmesh.verts)} e={len(self.bmesh.edges)} f={len(self.bmesh.faces)}') e.submit(lambda : gather(self.bmesh.verts, self.bmesh.edges, self.bmesh.faces, True)) else: if not self.split['gathered static']: #print(f' sv={len(self.split["static verts"])} se={len(self.split["static edges"])} sf={len(self.split["static faces"])}') e.submit(lambda : gather(self.split['static verts'], self.split['static edges'], self.split['static faces'], True)) self.split['gathered static'] = True #print(f' dv={len(self.split["dynamic verts"])} de={len(self.split["dynamic edges"])} df={len(self.split["dynamic faces"])}') e.submit(lambda : gather(self.split['dynamic verts'], self.split['dynamic edges'], self.split['dynamic faces'], False)) # @profiler.function def clean(self): if not self.buf_data_queue.empty(): tag_redraw_all('buffer update') while not self.buf_data_queue.empty(): data = self.buf_data_queue.get() if data == 'done': self._is_loading = False self._is_loaded = True self.async_load = False else: self.add_buffered_render(*data) try: # return if rfmesh hasn't changed self.rfmesh.clean() ver = self.rfmesh.get_version() if not self.always_dirty else None if self.rfmesh_version == ver: # profiler.add_note('--> is clean') return # profiler.add_note( # '--> versions: "%s", # "%s"' % (str(self.rfmesh_version), # str(ver)) # ) # make not dirty first in case bad things happen while drawing self.rfmesh_version = ver self._gather_data() except: Debugger.print_exception() # profiler.add_note('--> exception') pass # profiler.add_note('--> passed through') # @profiler.function def draw( self, view_forward, unit_scaling_factor, buf_matrix_target, buf_matrix_target_inv, buf_matrix_view, buf_matrix_view_invtrans, buf_matrix_proj, alpha_above, alpha_below, cull_backfaces, alpha_backface, draw_mirrored, symmetry=None, symmetry_view=None, symmetry_effect=0.0, symmetry_frame: Frame=None ): self.clean() if not self.buffered_renders_static and not self.buffered_renders_dynamic: return try: gpustate.depth_test('LESS_EQUAL') gpustate.depth_mask(False) # do not overwrite the depth buffer opts = dict(self.opts) opts['matrix model'] = self.rfmesh.xform.mx_p opts['matrix normal'] = self.rfmesh.xform.mx_n opts['matrix target'] = buf_matrix_target opts['matrix target inverse'] = buf_matrix_target_inv opts['matrix view'] = buf_matrix_view opts['matrix view normal'] = buf_matrix_view_invtrans opts['matrix projection'] = buf_matrix_proj opts['forward direction'] = view_forward opts['unit scaling factor'] = unit_scaling_factor opts['symmetry'] = symmetry opts['symmetry frame'] = symmetry_frame opts['symmetry view'] = symmetry_view opts['symmetry effect'] = symmetry_effect opts['draw mirrored'] = draw_mirrored bmegl.glSetDefaultOptions() opts['no warning'] = not options['warn non-manifold'] opts['no pinned'] = not options['show pinned'] opts['no seam'] = not options['show seam'] opts['cull backfaces'] = cull_backfaces opts['alpha backface'] = alpha_backface opts['dpi mult'] = self.drawing.get_dpi_mult() mirror_axes = self.rfmesh.mirror_mod.xyz if self.rfmesh.mirror_mod else [] for axis in mirror_axes: opts['mirror %s' % axis] = True if not opts.get('no below', False): # draw geometry hidden behind # geometry below opts['depth test'] = 'GREATER' # opts['depth mask'] = False opts['poly hidden'] = 1 - alpha_below opts['poly mirror hidden'] = 1 - alpha_below opts['line hidden'] = 1 - alpha_below opts['line mirror hidden'] = 1 - alpha_below opts['point hidden'] = 1 - alpha_below opts['point mirror hidden'] = 1 - alpha_below for buffered_render in chain(self.buffered_renders_static, self.buffered_renders_dynamic): buffered_render.draw(opts) # geometry above opts['depth test'] = 'LESS_EQUAL' # opts['depth mask'] = False opts['poly hidden'] = 1 - alpha_above opts['poly mirror hidden'] = 1 - alpha_above opts['line hidden'] = 1 - alpha_above opts['line mirror hidden'] = 1 - alpha_above opts['point hidden'] = 1 - alpha_above opts['point mirror hidden'] = 1 - alpha_above for buffered_render in chain(self.buffered_renders_static, self.buffered_renders_dynamic): buffered_render.draw(opts) gpustate.depth_test('LESS_EQUAL') gpustate.depth_mask(True) except: Debugger.print_exception() pass