Files
blender-portable-repo/scripts/addons/RetopoFlow/retopoflow/rfmesh/rfmesh_render.py
T
2026-03-17 14:30:01 -06:00

487 lines
21 KiB
Python

'''
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