Optional culling during navigation to increase FPS

- Only affects non-chunked instances (where verts are higher so the impact will be greater)
 - Things far away or not in view of the camera will be rendered as bounds
This commit is contained in:
Dion Moult
2024-02-25 23:11:28 +11:00
parent 0c4bcb8a12
commit 6f56d78e83
5 changed files with 125 additions and 0 deletions
@@ -55,6 +55,8 @@ classes = (
operator.aaa, operator.aaa,
operator.LoadLinkedProject, operator.LoadLinkedProject,
operator.QueryLinkedElement, operator.QueryLinkedElement,
operator.EnableCulling,
operator.DisableCulling,
prop.LibraryElement, prop.LibraryElement,
prop.FilterCategory, prop.FilterCategory,
prop.Link, prop.Link,
@@ -66,4 +66,5 @@ class ProjectData:
class LinksData: class LinksData:
linked_data = {} linked_data = {}
enable_culling = False
is_loaded = False is_loaded = False
@@ -38,6 +38,7 @@ from blenderbim.bim.ui import IFCFileSelector
from blenderbim.bim import import_ifc from blenderbim.bim import import_ifc
from blenderbim.bim import export_ifc from blenderbim.bim import export_ifc
from pathlib import Path from pathlib import Path
from mathutils import Vector, Matrix
from bpy.app.handlers import persistent from bpy.app.handlers import persistent
from blenderbim.bim.module.project.data import LinksData from blenderbim.bim.module.project.data import LinksData
from blenderbim.bim.module.project.decorator import ProjectDecorator from blenderbim.bim.module.project.decorator import ProjectDecorator
@@ -1913,3 +1914,109 @@ class QueryLinkedElement(bpy.types.Operator):
self.mouse_x = event.mouse_region_x self.mouse_x = event.mouse_region_x
self.mouse_y = event.mouse_region_y self.mouse_y = event.mouse_region_y
return self.execute(context) return self.execute(context)
class EnableCulling(bpy.types.Operator):
bl_idname = "bim.enable_culling"
bl_label = "Enable Culling"
bl_options = {"REGISTER"}
def __init__(self):
self.last_view_corners = None
self.total_mousemoves = 0
self.cullable_objects = []
def modal(self, context, event):
if not LinksData.enable_culling:
for obj in bpy.context.visible_objects:
if obj.type == "MESH" and obj.name.startswith("Ifc"):
obj.display_type = "SOLID"
self.cullable_objects = []
return {"CANCELLED"}
# Even if the view is changing, there are certain scenarios where we
# don't want to apply culling. For example, if we scroll to zoom but
# are simultaneously moving our mouse, or if we `Zoom to Selected`. A
# dumb but seemingly effective way is to count MOUSEMOVE events. If at
# least 3 consecutive events occur, you're probably not doing some
# other navigational thing.
if event.type == "MOUSEMOVE":
self.total_mousemoves += 1
else:
self.total_mousemoves = 0
if self.total_mousemoves > 2 and self.is_view_changed(context):
self.total_mousemoves = 0
camera_position = context.region_data.view_matrix.inverted().translation
for obj in self.cullable_objects:
if obj.type == "MESH" and obj.name.startswith("Ifc"):
if self.is_object_in_view(obj, context, camera_position):
obj.display_type = "SOLID"
elif obj.display_type != "BOUNDS":
obj.display_type = "BOUNDS"
return {"PASS_THROUGH"}
def is_view_changed(self, context):
view_matrix = context.region_data.view_matrix
projection_matrix = context.region_data.window_matrix
vp_matrix = projection_matrix @ view_matrix
# Get NDC coordinates of the viewport corners
viewport_corners = [Vector((-1, -1, 0)), Vector((1, -1, 0)), Vector((1, 1, 0)), Vector((-1, 1, 0))]
ndc_corners = [vp_matrix @ Vector((corner.x, corner.y, -1, 1)) for corner in viewport_corners]
ndc_corners = [(corner / corner.w).xy for corner in ndc_corners]
if self.last_view_corners != ndc_corners:
self.last_view_corners = ndc_corners
return True
return False
def is_object_in_view(self, obj, context, camera_position):
# Get the view matrix and the projection matrix from the active viewport
view_matrix = context.region_data.view_matrix
projection_matrix = context.region_data.window_matrix
# Calculate the combined view projection matrix
vp_matrix = projection_matrix @ view_matrix
obj_matrix_world = obj.matrix_world
# Transform each corner of the bounding box using the view projection matrix
# and check if it's inside the normalized device coordinates (NDC) space
for corner in [obj_matrix_world @ Vector(corner) for corner in obj.bound_box]:
ndc = vp_matrix @ corner.to_4d()
ndc /= ndc.w
if -1 <= ndc.x <= 1 and -1 <= ndc.y <= 1 and 0 <= ndc.z <= 1:
# At least one corner is inside the view, so the object is visible
break
else:
return False
# Check if the object is too far away from the camera
object_center = obj.matrix_world.translation
distance_threshold = 900 # 30m squared
if (camera_position - object_center).length_squared > distance_threshold:
# The object is too far away, so consider it not visible
return False
return True
def invoke(self, context, event):
LinksData.enable_culling = True
self.cullable_objects = []
for obj in bpy.context.visible_objects:
if obj.type == "MESH" and obj.name.startswith("Ifc") and max(obj.dimensions) < 0.6:
self.cullable_objects.append(obj)
camera_position = context.region_data.view_matrix.inverted().translation
if not self.is_object_in_view(obj, context, camera_position):
obj.display_type = "BOUNDS"
context.window_manager.modal_handler_add(self)
return {"RUNNING_MODAL"}
class DisableCulling(bpy.types.Operator):
bl_idname = "bim.disable_culling"
bl_label = "Disable Culling"
bl_options = {"REGISTER"}
def execute(self, context):
LinksData.enable_culling = False
return {"FINISHED"}
@@ -307,7 +307,13 @@ class BIM_PT_links(Panel):
"active_link_index", "active_link_index",
) )
if LinksData.enable_culling:
row = self.layout.row(align=True)
row.label(text="Object Culling Enabled", icon="MOD_TRIANGULATE")
if not LinksData.linked_data: if not LinksData.linked_data:
row = self.layout.row(align=True)
row.label(text="No Object Selected", icon="QUESTION")
return return
for name, value in LinksData.linked_data["attributes"].items(): for name, value in LinksData.linked_data["attributes"].items():
@@ -18,6 +18,7 @@
import bpy import bpy
from bpy.types import WorkSpaceTool from bpy.types import WorkSpaceTool
from blenderbim.bim.module.project.data import LinksData
class QueryTool(bpy.types.WorkSpaceTool): class QueryTool(bpy.types.WorkSpaceTool):
@@ -30,4 +31,12 @@ class QueryTool(bpy.types.WorkSpaceTool):
bl_widget = None bl_widget = None
bl_keymap = ( bl_keymap = (
("bim.query_linked_element", {"type": 'LEFTMOUSE', "value": 'PRESS'}, None), ("bim.query_linked_element", {"type": 'LEFTMOUSE', "value": 'PRESS'}, None),
# ("bim.project_hotkey", {"type": "C", "value": "PRESS", "alt": True}, {"properties": [("hotkey", "A_C")]}),
) )
def draw_settings(context, layout, ws_tool):
row = layout.row(align=True)
row.label(text="Query Object", icon="MOUSE_LMB")
row = layout.row(align=True)
row.label(text="", icon="EVENT_ALT")
row.label(text="Disable Culling" if LinksData.enable_culling else "Enable Culling", icon="EVENT_C")