/
Manifest
/
Real_Texture
Обзор
Документация
Войти
/
Manifest
/
Real_Texture
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
ToolsPythonForSequenceInUE/FrustumPTC.py
305 строк
12 KB
m.samodurov
init commit for The Invaders
17 фев 2026, 04:57
17 фев 2026, 04:57
4a668d4
Код
Авторство
О чём код?
import math import unreal class CameraFrustum(object): RGBY = [ unreal.LinearColor.RED, unreal.LinearColor.GREEN, unreal.LinearColor.BLUE, unreal.LinearColor.YELLOW, ] def __init__( self, camera_actor: unreal.CineCameraActor, time_delta=1, debug_info=False, properties_override=None, ): self.camera_actor = camera_actor self.camera_transform = camera_actor.get_actor_transform() self.camera_inv_transform = self.camera_transform.inverse() self.camera_view = self.get_camera_components().get_camera_view(time_delta) self.projection_matrix = self.camera_view.calculate_projection_matrix() self.debug_info = debug_info self.frustum_planes = None self.properties_override = properties_override or {} self.update_frustum() def update_frustum(self): self.frustum_planes = self._get_frustum_planes() @property def far_clip_plane(self): far_clip = self.properties_override.get("custom_far_clipping_plane") return far_clip or self.get_camera_components().ortho_far_clip_plane @far_clip_plane.setter def far_clip_plane(self, value): self.properties_override["custom_far_clipping_plane"] = value @property def near_clip_plane(self): camera_component = self.get_camera_components() if camera_component.override_custom_near_clipping_plane: near_clip = camera_component.custom_near_clipping_plane else: near_clip = camera_component.ortho_near_clip_plane return near_clip def _get_frustum_planes(self): near_clip = self.near_clip_plane far_clip = self.far_clip_plane tlv, trv, brv, blv = self._get_frustum_edges() def _vectors_to_plane(a, b, distance): normal = (a ^ b).normal() return unreal.Plane(*normal.to_tuple(), distance) planes = {} # order of cross product means orientation (inside) planes["top"] = _vectors_to_plane(trv, tlv, 0) planes["bottom"] = _vectors_to_plane(blv, brv, 0) planes["left"] = _vectors_to_plane(tlv, blv, 0) planes["right"] = _vectors_to_plane(brv, trv, 0) # camera +x oriented planes["near"] = unreal.Plane(1, 0, 0, near_clip) planes["far"] = unreal.Plane(-1, 0, 0, -far_clip) return planes # def _get_frustum_planes(self): # return { # plane: getattr(self.projection_matrix, f"get_frustum_{plane}_plane")() # for plane in CameraFrustum.plane_names # } def _points_tup_to_vector(self, pt): near, far = pt return far - near def _get_frustum_edges(self): return list(map(self._points_tup_to_vector, self._get_frustum_points())) def transform_vector(self, vector): return self.camera_transform.transform_location(vector) def _get_frustum_points(self): near_clip = self.near_clip_plane far_clip = self.far_clip_plane far_half_height, far_half_width = self.calculate_frame_from_fov(far_clip) near_half_height, near_half_width = self.calculate_frame_from_fov(near_clip) top_left_near = unreal.Vector(near_clip, -near_half_width, near_half_height) top_right_near = unreal.Vector(near_clip, near_half_width, near_half_height) bot_left_near = unreal.Vector(near_clip, -near_half_width, -near_half_height) bot_right_near = unreal.Vector(near_clip, near_half_width, -near_half_height) top_left_far = unreal.Vector(far_clip, -far_half_width, far_half_height) top_right_far = unreal.Vector(far_clip, +far_half_width, far_half_height) bot_left_far = unreal.Vector(far_clip, -far_half_width, -far_half_height) bot_right_far = unreal.Vector(far_clip, far_half_width, -far_half_height) near_points = [top_left_near, top_right_near, bot_right_near, bot_left_near] far_points = [top_left_far, top_right_far, bot_right_far, bot_left_far] return list(zip(near_points, far_points)) def draw_debug_frustum_lines(self): layer_subsystem = unreal.get_editor_subsystem(unreal.LayersSubsystem) world = layer_subsystem.get_world() # Assumptions: Camera is orthographic, Z axis is upwards, Y axis is right, X is forward for pt, color in zip(self._get_frustum_points(), self.RGBY): near, far = pt near = self.transform_vector(near) far = self.transform_vector(far) unreal.SystemLibrary.draw_debug_arrow( world, near, far, far.length(), color, duration=20, thickness=2 ) return # This function utilize unreal default draw_debug_frustum, but seems it is broken or i don't understand how to use # it # def draw_debug_frustum(self): # layer_subsystem = unreal.get_editor_subsystem(unreal.LayersSubsystem) # world = layer_subsystem.get_world() # unreal.SystemLibrary.draw_debug_frustum( # world, # self.camera_transform, # frustum_color=unreal.LinearColor.BLUE, # duration=20, # thickness=500, # ) def calculate_frame_from_fov(self, distance): hfov = self.get_camera_components().get_horizontal_field_of_view() vfov = self.get_camera_components().get_vertical_field_of_view() rad_half_hfov = math.radians(hfov / 2) rad_half_vfov = math.radians(vfov / 2) height = unreal.MathLibrary.tan(rad_half_vfov) * distance width = unreal.MathLibrary.tan(rad_half_hfov) * distance return height, width def draw_debug_frustum_planes(self, far_clip=1000, near_clip=None): layer_subsystem = unreal.get_editor_subsystem(unreal.LayersSubsystem) world = layer_subsystem.get_world() tlv, trv, brv, blv = self._get_frustum_edges() near_clip = near_clip or self.near_clip_plane # using default 1000 farclip, because cinecamera have 200K far clip that is almost infinity on visualisation far_half_height, far_half_width = self.calculate_frame_from_fov(far_clip) near_half_height, near_half_width = self.calculate_frame_from_fov(near_clip) colors = { cl: clv for cl, clv in zip(["left", "top", "right", "bottom"], self.RGBY) } for plane_name, plane in self.active_frustum_planes().items(): # map color to plane left red ... bottom yellow color = colors.get(plane_name, unreal.LinearColor.GRAY) center = { "top": (tlv + trv) / 2, "left": (tlv + blv) / 2, "right": (trv + brv) / 2, "bottom": (brv + blv) / 2, }.get(plane_name) offset = ( unreal.Vector(0, 0, 0) if plane_name in ("far", "near") else unreal.Vector(far_clip, 0, 0) ) size = { "right": far_half_height, "left": far_half_height, "top": far_half_width, "bottom": far_half_width, "far": max(far_half_height, far_half_width), "near": max(near_half_width, near_half_height), }[plane_name] new_plane_normal = self.camera_transform.transform_direction(plane).normal() if center is not None: # recalculating plane distance too, because of camera rotation + transform # basing on centers of far frustum frame, considering this center as plane point transformed_center = self.camera_transform.transform_location(center) w = new_plane_normal | transformed_center else: # TODO: seems that assumption is not correct, try to recalculate distance for center w = -far_clip if plane_name == "far" else near_clip new_plane = unreal.Plane(*new_plane_normal.to_tuple(), w) location = self.camera_transform.transform_location(offset) unreal.SystemLibrary.draw_debug_plane( world, new_plane or plane, location=location, size=size, plane_color=color, duration=20, ) def get_camera_components(self): return self.camera_actor.get_cine_camera_component() def active_frustum_planes(self): # There can be no far plane for frustum, because it is +infinity return {pn: p for pn, p in self.frustum_planes.items() if p is not None} def relativeToFrustum(self, pointsArray): numInside = 0 numPoints = len(pointsArray) active_planes = self.active_frustum_planes() for plane_name, plane in active_planes.items(): numBehindThisPlane = 0 for point in pointsArray: point_relation = self.relativeToPlane(plane, point) if self.debug_info: print( f"point {point} {'behind' if point_relation < 0 else 'upfront'} {plane_name} {plane}" ) if point_relation == -1: # Behind numBehindThisPlane += 1 if numBehindThisPlane == numPoints: ##all points were behind the same plane return False elif numBehindThisPlane == 0: numInside += 1 if numInside == len(active_planes.values()): return True # Inside return True # Intersect def relativeToPlane(self, plane, point): # projecting point on plane, calculate vector from prj point to original point and taking its normal # so calculating dot product of plane normal and point normal gives us cosine of angle between them # where 0 = 90 deg, [0, 1] = 0-90 deg, [-1, 0] = 90 - 180 deg, meaning normal to point is target behind plane plane_point = plane if plane.w == 0 else plane.multiply_float(plane.w) prj = point.project_point_on_to_plane(plane_point, plane) prj_to_point_normal = (point - prj).normal() val = plane | prj_to_point_normal if val > 0.0: return 1 # In front elif val < 0.0: return -1 # Behind return 0 # On the plane def is_actor_in_frustum(self, actor): origin, box_extent = actor.get_actor_bounds(False) x, y, z = box_extent.to_tuple() points = [ origin, origin + unreal.Vector(x, y, z), origin + unreal.Vector(x, y, -z), origin + unreal.Vector(x, -y, z), origin + unreal.Vector(x, -y, -z), origin + unreal.Vector(-x, y, z), origin + unreal.Vector(-x, y, z), origin + unreal.Vector(-x, -y, z), origin + unreal.Vector(-x, -y, -z), ] points = list(map(self.camera_inv_transform.transform_location, points)) if self.debug_info: actor_label = actor.get_actor_label() print(f"Checking if {actor_label} with origin {origin} in camera frustum") return self.relativeToFrustum(points) def test_camera_frustum_on_current_level_actors(selected=False): camera = None if selected: actors = unreal.get_editor_subsystem( unreal.LayersSubsystem ).get_selected_actors() else: actor_subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem) actors = actor_subsystem.get_all_level_actors() geo_actors = [] for actor in actors: if isinstance(actor, unreal.CineCameraActor): camera = actor else: geo_actors.append(actor) frustum = CameraFrustum(camera) frustum.draw_debug_frustum_lines() frustum.draw_debug_frustum_planes() for actor in geo_actors: actor_label = actor.get_actor_label() location = actor.get_actor_location() res = frustum.is_actor_in_frustum(actor) print(f"{actor_label} - {location} - in fru {res}") test_camera_frustum_on_current_level_actors(False)