2025-07-01
This commit is contained in:
@@ -0,0 +1,486 @@
|
||||
'''
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
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
|
||||
Reference in New Issue
Block a user