Benutzer-Werkzeuge

Webseiten-Werkzeuge


Seitenleiste

ss20:neg_drawables

Dies ist eine alte Version des Dokuments!


← Back to project page

Sourcecode

Download the release version here:
neg-world-engine.zip

Or download the newest version from the Git repository:
https://gitlab.tubit.tu-berlin.de/srather/NEG-World-Engine.git

Folder Structure:

Objects


drawable

drawable.py
import colors
from vector import *
 
 
class drawable(list):
    """
    Abstract superclass for all objects drawable on canvas.
    A drawable is a list of points ('Vector') with a 'color' attribute.
    """
 
 
    def __init__(self, *points, color=colors.BLACK):
        """
        Overrides '__init__' method of 'list' class.
        Will only accept point-like objects and adds 'color' attribute.
        """
 
        list.__init__(self)
        for point in points:
            drawable.append(self, point)
        self.color = color
 
 
    def append(self, item):
        """
        Overrides 'append' method of 'list' class.
        Will only accept point-like objects.
        """
 
        try:
            list.append(self, Vector(*item))
        except:
            raise TypeError(f"Expected vector/point, but got {item.__class__.__name__}")
 
 
    def __str__(self):
        """
        Override '__str__' method of 'list' class.
        Return string with classname, points in 'self' and additional attributes.
        """
 
        return f"{self.__class__.__name__}({list.__repr__(self)[1:-1]}) with {self.__dict__.__repr__()[1:-1].replace(':', ' =').replace(' ', '')}"
 
 
    def __repr__(self):
        """
        Override '__repr__' method of 'list' class.
        Return string with classname, points in 'self' and additional attributes.
        """
 
        return f"{self.__class__.__name__}({list.__repr__(self)[1:-1]}, {self.__dict__.__repr__()[1:-1].replace(':', ' =').replace(' ', '')})"
 
 
    def draw(self, screen, offset):
        """Abstract method to draw 'self' on 'screen'."""
        raise NotImplementedError
 
 
    def draw_shadow(self, screen, offset, source, color=(31,31,31)):
        """Abstract method to draw a shadow of 'self' on 'screen'."""
        raise NotImplementedError
 
 
    def dist_to(self, source):
        """Abstract method to calculate the distance to the player ('source')."""
        raise NotImplementedError
 
 
    def cut(self, source, portal):
        """Abstract method to cut 'self' to fit in the portal view."""
        raise NotImplementedError

↑ Back to top


Ray

ray.py
import pygame.gfxdraw
import numpy
from drawable import *
from matrix import *
 
 
class Ray(drawable):
    """
    Ray class as drawable object for visual debug. A Ray is a vector with
    a starting point, visualised as a line with a startpoint, but no end.
 
    Create a new 'Ray' object using 'ray = Ray((x1, y1), (x2, y2))'
    or if the points are in a list like '[(x1, y1), (x2, y2)]'
    use 'ray = Ray(*[(x1, y1), (x2, y2)])' to unpack it.
    Note that 'Ray(*ray)' creates a copy of 'ray'.
    """
 
 
    def __init__(self, start, direction, color=(255,0,0)):
        """Call the init function of parent class 'drawable'."""
        drawable.__init__(self, start, direction, color=color)
 
 
    # Overrides attribute access
    def __getattr__(self, attr):
        """
        Return 'self[0]' and 'self[1]' as attribute 'start' and 'dir'.
        """
 
        if attr == 'start':
            return self[0]
        if attr == 'dir':
            return self[1]
        if attr == 'color':
            return self.__dict__['color']
 
        raise AttributeError(f"{self.__class__.__name__} has no Attribute {attr}.")
 
 
    def point_at(self, value):
        """
        Return the point 'value' times 'self.dir' from 'self.start'.
        """
 
        if type(value) in (int, float):
            return self.start + value * self.dir
 
        raise TypeError(f"Expected int or float, but {value.__class__.__name__} found")
 
 
    def value_at(self, point):
        """
        Return the how many 'self.dir' 'point' is from 'self.start'.
        """
 
        if type(point) is Vector:
            if self.dir.multiple_of(point - self.start):
                value = (point - self.start) / self.dir
                if value is not None:
                    return value
 
            h = (point - self.start).dot(self.dir) / self.dir.dot(self.dir)
            return h
 
            print(f"Warning: returned 'None' for value_at of point {((point - self.start) - h * self.dir).length()} off the ray)")
            return None
 
        raise TypeError(f"Expected Vector, but {point.__class__.__name__} found")
 
 
    def offscreen(self, screen, offset):
        """
        Return a point of the ray that is definetly not on 'screen'.
        """
 
        if not self.dir: # == 0
            return self.start
 
        x, y = float('inf'), float('inf')
 
        if self.dir.x > 0:
            x = (offset.x + screen.get_rect()[2] / 2 - self.start.x) / self.dir.x
        elif self.dir.x < 0:
            x = (offset.x - screen.get_rect()[2] / 2 - self.start.x) / self.dir.x
 
        if self.dir.y > 0:
            y = (offset.y + screen.get_rect()[3] / 2 - self.start.y) / self.dir.y
        elif self.dir.y < 0:
            y = (offset.y - screen.get_rect()[3] / 2 - self.start.y) / self.dir.y
 
        return self.point_at(max(0, min(x, y)))
 
 
    # Implements intersect() from abstract drawable class
    def intersect(self, other):
        """
        Return list of intersections sorted by distance to 'self.start'.
        """
 
        if self.dir.length() == 0:
            return []
 
        if isinstance(other, Vector):
            if self.dir.multiple_of(other - self.start): # point on self
                if self.value_at(other) is not None:
                    return [other]
 
            return []
 
        if isinstance(other, drawable):
            if type(other) is Ray:
                if self.dir.multiple_of(other.dir): # linear depentant
                    return self.intersect(other.start)
 
                if self.dir.x == 0: # switch vectors for gauss algorithm
                    return other.intersect(self)
 
                matrix = [
                    [self.dir.x, -other.dir.x, other.start.x - self.start.x],
                    [self.dir.y, -other.dir.y, other.start.y - self.start.y],
                ]
 
                # gauss algorithm
                matrix[0][1] /= matrix[0][0]
                matrix[0][2] /= matrix[0][0]
                matrix[1][1] -= matrix[1][0] * matrix[0][1]
                matrix[1][2] -= matrix[1][0] * matrix[0][2]
                matrix[1][2] /= matrix[1][1]
                matrix[0][2] -= matrix[0][1] * matrix[1][2]
 
                if matrix[0][2] >= 0 and matrix[1][2] >= 0:
                    return [self.point_at(matrix[0][2])]
                return []
 
 
            if type(other) is Polygon:
                if len(other) == 1:
                    return self.intersect(*other)
 
                if len(other) >= 2:
                    lines = []
                    for i, point in other[:-1]:
                        lines.append(Ray(other[i], other[i+1] - other[i]))
                    if len(other) >= 3:
                        lines.append(Ray(other[-1], other[0] - other[-1]))
 
                    points = []
                    for line in lines:
                        point = self.intersect(line)
                        if len(point) == 1 and point[0] / line.dir <= 1:
                            points += point
 
                    values = []
                    for point in points:
                        values.append(self.value_at(point))
                    values.sort()
 
                    intersections = []
                    for value in values:
                        intersections.append(self.point_at(value))
 
                    return intersections
 
                return [] # for empty polygons
 
            raise NotImplementedError # for other drawables
 
        raise TypeError(f"Expected drawable objects, but '{other.__class__.__name__}' found")
 
 
    # Implements draw() method of abstract drawable class
    def draw(self, screen, offset):
        """
        Visualizes 'self' as a line with a startpoint, but no end.
        """
 
        ray = Ray(self.start, self.offscreen(screen, offset))
 
        # move to real position on screen
        rect = Vector(*screen.get_rect()[2:])
        for i, _ in enumerate(ray):
            ray[i] = round(ray[i] - offset + rect / 2)
 
        if ray.dir: # != 0
            pygame.draw.aaline(screen, self.color, ray[0], ray[1])

↑ Back to top


Polygon

polygon.py
from drawable import *
from ray import *
 
 
class Polygon(drawable):
    """
    Polygon class as drawable object. Implements functionality of abstract drawable class.
    """
 
 
    # constructor: create self as drawable object
    def __init__(self, *points, color=(0,0,0)):
        drawable.__init__(self, *points, color=color)
 
 
    # Implements draw() method of abstract drawable class
    def draw(self, screen, offset):
        if not self: # empty
            return
 
        polygon = self.copy()
 
        # move to 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:
                return
 
        if len(polygon) == 1:
            pygame.gfxdraw.pixel(screen, *polygon[0], self.color)
        elif len(polygon) == 2:
            pygame.draw.aaline(screen, self.color, polygon[0], polygon[1])
        else:
            pygame.gfxdraw.aapolygon(screen, polygon, self.color)
            pygame.gfxdraw.filled_polygon(screen, polygon, self.color)
 
 
    # Implements draw_shadow() from abstract drawable class
    def draw_shadow(self, screen, offset, source, color=(31,31,31)):
        if len(self) == 0:
            return
 
        elif len(self) == 1:
            Ray(self[0], self[0] - source, color=color).draw(screen, offset)
 
        elif len(self) == 2:
            if not self[0].onscreen(screen, offset) and not self[1].onscreen(screen, offset):
                return
 
            if self[0] == source or self[1] == source:
                for point in self:
                    Ray(point, point - source, color=color).draw(screen, offset)
            else:
                rect = screen.get_rect()
                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),
                ]
 
                ray1 = Ray(self[0], self[0] - source)
                ray2 = Ray(self[1], self[1] - source)
                if ray1.dir.angle_to(ray2.dir) > 180:
                    ray1, ray2 = ray2, ray1
 
                shadow = Polygon(color=color)
                shadow.append(ray2.offscreen(screen, offset))
                shadow.append(ray2.start)
                shadow.append(ray1.start)
                shadow.append(ray1.offscreen(screen, offset))
 
                for i, _ in enumerate(corners[1:-1]):
                    if corners[i].is_between(ray1.dir, ray2.dir):
                        if corners[i-1].is_between(ray1.dir, ray2.dir):
                            shadow.append(offset + corners[i-1])
                        shadow.append(offset + corners[i])
                        if corners[i+1].is_between(ray1.dir, ray2.dir):
                            shadow.append(offset + corners[i+1])
                        break
 
                shadow.draw(screen, offset)
 
        else:
            for i, _ in enumerate(self[:-1]):
                Polygon(self[i], self[i+1]).draw_shadow(screen, offset, source)
            Polygon(self[-1], self[0]).draw_shadow(screen, offset, source)
 
 
    # Implements dist_to() method of abstract drawable class
    def dist_to(self, source):
        def sdf_line(a, b, p):
            b_a = b - a
            p_a = p - a
            if b == a:
                h = 0
            else:
                h = min(1, max(0, p_a.dot(b_a) / b_a.dot(b_a)))
            return (p_a - h * b_a).length()
 
        if len(self) == 0:
            return float('inf')
 
        if len(self) == 1:
            return (source - self[0]).length()
 
        else:
            m = float('inf')
            for i, _ in enumerate(self):
                m = min(m, sdf_line(self[i-1], self[i], source))
            return m
 
 
    # Implements cut() from abstract drawable class
    def cut(self, source, portal):
        def position(point):
            vec = point - source
            if vec.is_between(ray1.dir, ray2.dir):
                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
 
        ray1 = Ray(portal[0], portal[0] - source)
        ray2 = Ray(portal[1], portal[1] - source)
        border = Ray(ray1.start, ray2.start - ray1.start)
        if ray1.dir.angle_to(ray2.dir) > 180:
            ray1, ray2 = ray2, ray1
        rest = Polygon()
        rest.__dict__ = self.__dict__
        null = -(ray1.dir + ray2.dir)
 
        if len(self) == 0:
            return rest
 
        if len(self) == 2:
            if self[0] in portal and self[1] in portal:
                return rest
 
        nxt_pos = position(self[-1])
        for i, _ in enumerate(self):
            cur_pos = nxt_pos
            nxt_pos = position(self[i])
 
            if cur_pos == 1:
                rest.append(self[i-1])
 
            if nxt_pos == cur_pos:
                continue
 
            inter = []
 
            if 1 in (cur_pos, nxt_pos):
                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]))
 
            elif -1 in (cur_pos, nxt_pos):
                if 0 in (cur_pos, nxt_pos):
                    inter += [ray1.start]
                else: # 2
                    inter += [ray2.start]
 
            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]))
 
            for pt in inter:
                if pt.length2() > 10000:
                    print(f"CalculationError while cutting {self} at {ray1, ray2}:\n    {pt} is very big")
                else:
                    rest.append(pt)
 
        if nxt_pos == 1:
            rest.append(self[-1])
 
        return rest

↑ Back to top


Floor

floor.py
import colors
from polygon import *
 
class Floor(Polygon):
    """
    Class for polygons just drawn on the floor as drawable object. Implements functionality of abstract drawable class.
    """
 
 
    # constructor: create self as drawable object
    def __init__(self, *points, color=colors.DARK_RED):
        Polygon.__init__(self, *points, color=color)
 
 
    # Implements draw_shadow() from abstract drawable class
    def draw_shadow(self, screen, offset, source, color=None):
        pass
 
 
    # Implements cut() from abstract drawable class
    def cut(self, source, portal):
        rest = Polygon.cut(self, source, portal)
        if len(rest) > 2:
            return Floor(*rest, color=self.color)
 
        return Polygon()
 
 
    def dist_to(self, source):
        return float('inf')

↑ Back to top


Portal

portal.py
from polygon import *
from matrix import *
import colors
 
 
class Portal(Polygon):
    """
    Portal class as drawable object. Implements functionality of abstract drawable class.
    """
 
 
    # constructor: create self as Polygon object
    def __init__(self, start, end, M=((1,0),(0,1)), M2=None, offset=None, offset2=None, color=(0,0,255)):
        Polygon.__init__(self, start, end, color=color)
        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)
 
 
    # Overrides attribute access
    def __getattr__(self, attr):
        """
        Return 'self[0]' and 'self[1]' as attribute 'start' and 'end'.
        """
 
        if attr == 'start':
            return self[0]
        if attr == 'end':
            return self[1]
        if attr == 'M':
            return self.__dict__['M']
        if attr == 'offset':
            return self.__dict__['offset']
        if attr == 'color':
            return self.__dict__['color']
 
        raise AttributeError(f"{self.__class__.__name__} has no Attribute {attr}.")
 
 
    def go_through(self, dir, world):
        portal_world = []
 
        for object in world:
            port = object.__class__(*object)
            port.__dict__ = object.__dict__
 
            if dir.is_between(self.end - self.start, self.start - self.end):
                for i, _ in enumerate(object):
                    if self.offset is None:
                        offset = self.M.prod((self.start + self.end) / 2) - (self.start + self.end) / 2
                    else:
                        offset = self.offset
 
                    port[i] = self.M.prod(object[i]) - offset
 
                if type(object) is Portal:
                    if self.M.i.angle_to(self.M.j) > 180:
                        port[0], port[1] = port[1], port[0]
            else:
                for i, _ in enumerate(object):
                    if self.offset2 is None:
                        offset2 = self.M2.prod((self.start + self.end) / 2) - (self.start + self.end) / 2
                    else:
                        offset2 = self.offset2
 
                    port[i] = self.M2.prod(object[i]) - offset2
 
                if type(object) is Portal:
                    if self.M2.i.angle_to(self.M2.j) > 180:
                        port[0], port[1] = port[1], port[0]
 
            portal_world.append(port)
 
        return portal_world
 
 
    def portal_world(self, world, source):
        portal_world = []
 
        for object in world:
            port = object.__class__(*object)
            port.__dict__ = object.__dict__
 
            if (self.start - source).is_between(self.end - self.start, self.start - self.end):
                for i, _ in enumerate(object):
                    if self.offset is None:
                        offset = self.M.prod((self.start + self.end) / 2) - (self.start + self.end) / 2
                    else:
                        offset = self.offset
 
                    port[i] = self.M.prod(object[i]) - offset
 
                if type(object) is Portal:
                    if self.M.i.angle_to(self.M.j) > 180:
                        port[0], port[1] = port[1], port[0]
            else:
                for i, _ in enumerate(object):
                    if self.offset2 is None:
                        offset2 = self.M2.prod((self.start + self.end) / 2) - (self.start + self.end) / 2
                    else:
                        offset2 = self.offset2
 
                    port[i] = self.M2.prod(object[i]) - offset2
 
                if type(object) is Portal:
                    if self.M2.i.angle_to(self.M2.j) > 180:
                        port[0], port[1] = port[1], port[0]
 
            cut = port.cut(source, self)
            if cut: # not empty
                portal_world.append(cut)
 
        return portal_world
 
 
    def draw(self, screen, offset):
        pass
 
    # overrides draw_shadow() inherited from Polygon class
    def draw_shadow(self, screen, offset, source):
        """
        Draws shadow of portal
        Called by 'self.draw_shadow(screen, offset)'
        """
 
        Polygon.draw_shadow(self, screen, offset, source, color=colors.GRAY)
        # Polygon.draw_shadow(self, screen, offset, source, color=(150,150,150))
 
        for point in self:
            Ray(point, point - source, color=(0,0,0)).draw(screen, offset)
 
 
    # Implements cut() from abstract drawable class
    def cut(self, source, portal):
        rest = Polygon.cut(self, source, portal)
        if len(rest) >= 2:
            return Portal(*reversed(rest[:2]), M=self.M, color=self.color)
 
        return Polygon()

↑ Back to top

ss20/neg_drawables.1599827217.txt.gz · Zuletzt geändert: 2020/09/11 14:26 von srather