Hier werden die Unterschiede zwischen zwei Versionen gezeigt.
Beide Seiten der vorigen Revision Vorhergehende Überarbeitung | |||
ss20:neg_drawables [2020/09/11 14:27] srather [Portal] |
ss20:neg_drawables [2020/09/11 14:28] (aktuell) srather [Portal] |
||
---|---|---|---|
Zeile 552: | Zeile 552: | ||
<file python portal.py> | <file python portal.py> | ||
import colors | import colors | ||
- | from drawable import * | + | from polygon import * |
- | from ray import * | + | from matrix import * |
- | class Polygon(drawable): | + | class Portal(Polygon): |
""" | """ | ||
- | Polygon class as 'drawable' objects. | + | Portal class as a 2-element 'Polygon' object. |
- | Implements functionality of abstract drawable class. | + | In for both sides of the portal there is |
+ | an offset vector and a distortion matrix. | ||
""" | """ | ||
- | def __init__(self, *points, color=colors.BLACK): | + | # constructor: create self as Polygon object |
- | """Initilise 'self' with standard color 'BLACK'.""" | + | def __init__(self, start, end, M=((1,0),(0,1)), M2=None, offset=None, offset2=None, color=colors.BRIGHT_BLUE): |
- | drawable.__init__(self, *points, color=color) | + | |
- | + | ||
- | + | ||
- | def draw(self, screen, offset): | + | |
""" | """ | ||
- | Draw 'self' as a polygon on 'screen'. | + | Initilises 'self' with standard color 'BRIGHT_BLUE'. |
+ | offset -- for side 1 (std: None == portal will remain in same place) | ||
+ | offset2 -- for side 2 (std: None == portal will remain in same place) | ||
+ | M -- distortion matrix for side 1 (std: unit matrix) | ||
+ | M2 -- distortion matrix for side 2 (std: None == inverse of M) | ||
""" | """ | ||
- | if not self: # empty | + | Polygon.__init__(self, start, end, color=color) |
- | return | + | |
- | polygon = self.copy() | + | self.M = Matrix(*M) |
+ | if M2 is None: | ||
+ | self.M2 = self.M.inverse() | ||
+ | else: | ||
+ | self.M2 = Matrix(*M2) | ||
+ | if offset is None: | ||
+ | self.offset = None | ||
+ | else: | ||
+ | self.offset = Vector(*offset) | ||
+ | if offset2 is None: | ||
+ | self.offset2 = None | ||
+ | else: | ||
+ | self.offset2 = Vector(*offset2) | ||
- | # calculate real position on screen | ||
- | rect = Vector(*screen.get_rect()[2:]) | ||
- | for i, _ in enumerate(polygon): | ||
- | polygon[i] = round(polygon[i] - offset + rect / 2) | ||
- | if polygon[i].length2() > 30000: # hotfix | ||
- | return | ||
- | # point | + | def __getattr__(self, attr): |
- | if len(polygon) == 1: | + | """ |
- | pygame.gfxdraw.pixel(screen, *polygon[0], self.color) | + | Return 'self[0]' and 'self[1]' as attribute 'start' and 'end'. |
- | # line | + | """ |
- | elif len(polygon) == 2: | + | |
- | pygame.draw.aaline(screen, self.color, polygon[0], polygon[1]) | + | if attr == 'start': |
- | # polygon | + | return self[0] |
- | else: | + | if attr == 'end': |
- | pygame.gfxdraw.aapolygon(screen, polygon, self.color) | + | return self[1] |
- | pygame.gfxdraw.filled_polygon(screen, polygon, self.color) | + | |
- | def draw_shadow(self, screen, offset, source, color=colors.DARK_GRAY): | + | def go_through(self, dir, world): |
""" | """ | ||
- | Draw draw a shadow of 'self' on 'screen' respecting the light-'source'. | + | Return the portal world of 'world' for the side in direction 'dir'. |
""" | """ | ||
- | if not self: # empty | + | portal_world = [] |
- | return | + | |
- | # point | + | for object in world: |
- | elif len(self) == 1: | + | port = object.__class__(*object) |
- | Ray(self[0], self[0] - source, color=color).draw(screen, offset) | + | port.__dict__ = object.__dict__ |
- | # line | + | # gone through side 1 |
- | elif len(self) == 2: | + | if dir.is_between(self.end - self.start, self.start - self.end): |
- | # edgecase: would lead to DivisionByZeroError | + | for i, _ in enumerate(object): |
- | if self[0] == source or self[1] == source: | + | if self.offset is None: |
- | for point in self: | + | # middle of portal in portal world |
- | Ray(point, point - source, color=color).draw(screen, offset) | + | offset = self.M.prod((self.start + self.end) / 2) - (self.start + self.end) / 2 |
- | return | + | else: |
+ | offset = self.offset | ||
- | # corners of screen area | + | # apply distortion and offset |
- | rect = screen.get_rect() | + | port[i] = self.M.prod(object[i]) - offset |
- | corner = Vector(*rect[2:]) / 2 | + | |
- | corners = [ | + | |
- | corner * Vector( 1, 1), | + | |
- | corner * Vector(-1, 1), | + | |
- | corner * Vector(-1,-1), | + | |
- | corner * Vector( 1,-1), | + | |
- | corner * Vector( 1, 1), | + | |
- | corner * Vector( 1,-1), | + | |
- | ] | + | |
- | # rays from source to both ends | + | |
- | ray1 = Ray(self[0], self[0] - source) | + | |
- | ray2 = Ray(self[1], self[1] - source) | + | |
- | # garantees anticlockwise order (important for calculations) | + | |
- | if ray1.dir.angle_to(ray2.dir) > 180: | + | |
- | ray1, ray2 = ray2, ray1 | + | |
- | # creates minimal polygon | + | # if world gets mirrored the side-sensitive portals have to be reverted |
- | shadow = Polygon(color=color) | + | if type(object) is Portal: |
- | shadow.append(ray2.offscreen(screen, offset)) | + | if self.M.i.angle_to(self.M.j) > 180: |
- | shadow.append(ray2.start) | + | port[0], port[1] = port[1], port[0] |
- | shadow.append(ray1.start) | + | |
- | shadow.append(ray1.offscreen(screen, offset)) | + | |
- | # adds corners of screen to polygon if needed | + | # gone through side 2 |
- | for i, _ in enumerate(corners[1:-1]): | + | else: |
- | if corners[i].is_between(ray1.dir, ray2.dir): | + | for i, _ in enumerate(object): |
- | if corners[i-1].is_between(ray1.dir, ray2.dir): | + | if self.offset2 is None: |
- | shadow.append(offset + corners[i-1]) | + | # middle of portal in portal world |
- | shadow.append(offset + corners[i]) | + | offset2 = self.M2.prod((self.start + self.end) / 2) - (self.start + self.end) / 2 |
- | if corners[i+1].is_between(ray1.dir, ray2.dir): | + | else: |
- | shadow.append(offset + corners[i+1]) | + | offset2 = self.offset2 |
- | break | + | |
- | # finally draws calculated shadow | + | # apply distortion and offset |
- | shadow.draw(screen, offset) | + | port[i] = self.M2.prod(object[i]) - offset2 |
- | # polygon | + | # if world gets mirrored the side-sensitive portals have to be reverted |
- | else: # draws shadow for every line in polygon | + | if type(object) is Portal: |
- | for i, _ in enumerate(self[:-1]): | + | if self.M2.i.angle_to(self.M2.j) > 180: |
- | Polygon(self[i], self[i+1]).draw_shadow(screen, offset, source) | + | port[0], port[1] = port[1], port[0] |
- | Polygon(self[-1], self[0]).draw_shadow(screen, offset, source) | + | |
+ | # add distorted object to portal world | ||
+ | portal_world.append(port) | ||
- | def dist_to(self, source): | + | return portal_world |
- | """ | + | |
- | Return euclidean distance to the player ('source'). | + | |
- | """ | + | |
- | def sdf_line(a, b, p): | ||
- | """ | ||
- | Return distance from point p to line a-b. | ||
- | (source: https://youtu.be/PMltMdi1Wzg) | ||
- | """ | ||
- | if b == a: | + | def portal_world(self, world, source): |
- | h = 0 | + | portal_world = [] |
- | else: | + | |
- | h = min(1, max(0, (p-a).dot(b-a) / (b-a).dot(b-a))) | + | |
- | return ((p-a) - h * (b-a)).length() | + | |
- | if not self: # empty | + | for object in world: |
- | return float('inf') | + | port = object.__class__(*object) |
+ | port.__dict__ = object.__dict__ | ||
- | # minimum distance to a side | + | # gone through side 1 |
- | m = float('inf') | + | if (self.start - source).is_between(self.end - self.start, self.start - self.end): |
- | for i, _ in enumerate(self): | + | for i, _ in enumerate(object): |
- | m = min(m, sdf_line(self[i-1], self[i], source)) | + | if self.offset is None: |
- | return m | + | # middle of portal in portal world |
+ | offset = self.M.prod((self.start + self.end) / 2) - (self.start + self.end) / 2 | ||
+ | else: | ||
+ | offset = self.offset | ||
+ | # apply distortion and offset | ||
+ | port[i] = self.M.prod(object[i]) - offset | ||
- | def cut(self, source, portal): | + | # if world gets mirrored the side-sensitive portals have to be reverted |
- | """ | + | if type(object) is Portal: |
- | Return 'self' but cut to fit in the view from 'source' though 'portal'. | + | if self.M.i.angle_to(self.M.j) > 180: |
- | This algorithm isn't perfect, but the best I got at the moment. | + | port[0], port[1] = port[1], port[0] |
- | """ | + | |
- | def position(point): | + | # gone through side 2 |
- | """ | + | else: |
- | Return the area 'point' is in: | + | for i, _ in enumerate(object): |
- | -1 -- in the triangle between source and portal line | + | if self.offset2 is None: |
- | 0 -- to the right of visible area | + | # middle of portal in portal world |
- | 1 -- in the visible portal area | + | offset2 = self.M2.prod((self.start + self.end) / 2) - (self.start + self.end) / 2 |
- | 2 -- to the left of visible area | + | else: |
- | """ | + | offset2 = self.offset2 |
- | vec = point - source | + | # apply distortion and offset |
- | if vec.is_between(ray1.dir, ray2.dir): | + | port[i] = self.M2.prod(object[i]) - offset2 |
- | if (point - ray1.start).is_between(ray1.dir, border.dir): | + | |
- | return 1 | + | |
- | else: | + | |
- | return -1 | + | |
- | if vec.is_between(null, ray1.dir): | + | |
- | return 0 | + | |
- | if vec.is_between(ray2.dir, null): | + | |
- | return 2 | + | |
- | # rays that are borders of different areas | + | # if world gets mirrored the side-sensitive portals have to be reverted |
- | ray1 = Ray(portal.start, portal.start - source) # area 0 <--> 1 | + | if type(object) is Portal: |
- | ray2 = Ray(portal.end, portal.end - source) # area 1 <--> 2 | + | if self.M2.i.angle_to(self.M2.j) > 180: |
- | border = Ray(ray1.start, ray2.start - ray1.start) # area -1 <--> 1 | + | port[0], port[1] = port[1], port[0] |
- | null = -(ray1.dir + ray2.dir) # area 0 <--> 2 | + | |
- | # garantees anticlockwise order (important for calculations) | + | |
- | if ray1.dir.angle_to(ray2.dir) > 180: | + | |
- | ray1, ray2 = ray2, ray1 | + | |
- | rest = Polygon() | + | # add distorted and truncated object to portal world |
- | rest.__dict__ = self.__dict__ | + | cut = port.cut(source, self) |
+ | if cut: # not empty | ||
+ | portal_world.append(cut) | ||
- | if not self: # empty | + | return portal_world |
- | return rest | + | |
- | # edgecase for when a portal is in its own portal world in the same place | ||
- | if len(self) == 2: | ||
- | if self[0] in portal and self[1] in portal: | ||
- | return rest | ||
- | # algorithm for cutting polygon | + | def draw(self, screen, offset): |
- | nxt_pos = position(self[-1]) | + | """ |
- | for i, _ in enumerate(self): | + | Draw the endpoints of 'self' on 'screen'. |
- | cur_pos = nxt_pos | + | """ |
- | nxt_pos = position(self[i]) | + | |
- | if cur_pos == 1: # visible | + | for point in self: |
- | rest.append(self[i-1]) | + | Polygon(point).draw(screen, offset) |
- | # same position, nothing changes | ||
- | if nxt_pos == cur_pos: | ||
- | continue | ||
- | # different position, intersections with visible area needed | + | def draw_shadow(self, screen, offset, source): |
- | inter = [] | + | """ |
+ | Draw draw a shadow of 'self' on 'screen' respecting the light-'source'. | ||
+ | """ | ||
- | # through the left or right boundary of visible area -> intersection | + | # draw the shadow in the same color as the background to wipe the canvas |
- | if 1 in (cur_pos, nxt_pos): | + | Polygon.draw_shadow(self, screen, offset, source, color=colors.GRAY) |
- | if 0 in (cur_pos, nxt_pos): | + | |
- | inter += ray1.intersect(Ray(self[i-1], self[i] - self[i-1])) | + | |
- | elif 2 in (cur_pos, nxt_pos): | + | |
- | inter += ray2.intersect(Ray(self[i-1], self[i] - self[i-1])) | + | |
- | if not inter: # empty | + | |
- | inter += border.intersect(Ray(self[i-1], self[i] - self[i-1])) | + | |
- | # line goes outside the visible area around the portal -> edges of portal | + | # draw shadows for the end points of portal |
- | elif -1 in (cur_pos, nxt_pos): | + | for point in self: |
- | if 0 in (cur_pos, nxt_pos): | + | Polygon(point).draw_shadow(screen, offset, source) |
- | inter += [ray1.start] | + | |
- | else: # 2 | + | |
- | inter += [ray2.start] | + | |
- | # line goes from the left to the right -> may intersect visible area | ||
- | else: # 0 and 2 | ||
- | if cur_pos == 0: | ||
- | inter += ray1.intersect(Ray(self[i-1], self[i] - self[i-1])) | ||
- | inter += ray2.intersect(Ray(self[i-1], self[i] - self[i-1])) | ||
- | else: # 2 | ||
- | inter += ray2.intersect(Ray(self[i-1], self[i] - self[i-1])) | ||
- | inter += ray1.intersect(Ray(self[i-1], self[i] - self[i-1])) | ||
- | # add intersections | + | def cut(self, source, portal): |
- | for pt in inter: | + | """ |
- | if pt.length2() > 10000: # hotfix | + | Return 'self' but cut to fit in the view from 'source' though 'portal'. |
- | print(f"CalculationError while cutting {self} at {ray1, ray2}:\n {pt} is very big") | + | Makes use of 'Polygon.cut'. |
- | else: | + | """ |
- | rest.append(pt) | + | |
+ | rest = Polygon.cut(self, source, portal) | ||
+ | if len(rest) == 2: | ||
+ | return Portal(*reversed(rest[:2]), M=self.M, color=self.color) | ||
- | return rest | + | return Polygon() |
</file> | </file> |