Wednesday, 2 August 2023

OpenGL Depth Map renders objects only 1 unit far away

I've edited this question as I got a wrong interpretation of what was previously 'wrong' with my code. Previously I thought I made a mistake in the FBO or the MVP-matrix as my shadow map was completely blank, but in reality I did everything right and the depth map renders things only from a distance of 0-1 correlating the black and white pixels on the screen. Silly me.

I've scaled my whole scene down and repositioned my 'sun' so that I can now see the shadows. My question is how I can 'extend' this view so that not only objects as far away as 1 unit can be seen but as far as my view goes.

Also my shadow map is in black and white opposed to the red and black of the tutorial I followed. What's up with that?

And lastly the view of the 'sun' doesn't update with me moving. Expected it would follow the character, updating the view and visible shadows, but the view stays stationary at the position of the 'sun'.

Here's my whole Shadow Box class as this is probably the problem with that:

Edit: It's probably not the problem, but you still can have a look at it.

class ShadowBox:
    """
    Represents the 3D cuboidal area of the world in which objects will cast
    shadows (basically represents the orthographic projection area for the shadow
    render pass). It is updated each frame to optimise the area, making it as
    small as possible (to allow for optimal shadow map resolution) while not
    being too small to avoid objects not having shadows when they should.
    Everything inside the cuboidal area represented by this object will be
    rendered to the shadow map in the shadow render pass. Everything outside the
    area won't be.
    """
    __OFFSET = 10
    __UP = vec4(0, 1, 0, 0)
    __FORWARD = vec4(0, 0, -1, 0)
    __SHADOW_DISTANCE = 100

    def __init__(self, light_view_matrix: mat4, camera):
        """
        Creates a new shadow box and calculates some initial values relating to
        the camera's view frustum, namely the width and height of the near plane
        and (possibly adjusted) far plane.

        :param light_view_matrix:
        basically the "view matrix" of the light. Can be used to transform a point from world space into "light" space
        (i.e. changes a point's coordinates from being in relation to the world's axis to being in terms of the light's
        local axis).

        :param camera:
        the in-game camera.
        """

        self.__light_view_matrix = light_view_matrix
        self.__cam = camera

        self.__min_x = None
        self.__max_x = None
        self.__min_y = None
        self.__max_y = None
        self.__min_z = None
        self.__max_z = None

        self.__far_height = None
        self.__far_width = None
        self.__near_height = None
        self.__near_width = None

        self.calculate_widths_and_heights()

    def update(self) -> None:
        """
        Updates the bounds of the shadow box based on the light direction and the
        camera's view frustum, to make sure that the box covers the smallest area
        possible while still ensuring that everything inside the camera's view
        (within a certain range) will cast shadows.
        :return:
        """

        rotation: mat4 = self.calculate_camera_rotation_matrix()
        forward_vector = rotation * self.__FORWARD
        forward_vector = vec3(forward_vector.x, forward_vector.y, forward_vector.z)

        to_far = forward_vector * self.__SHADOW_DISTANCE
        to_near = forward_vector * master_renderer.MasterRenderer.get_near_plane()
        center_near = to_near + vec3(self.__cam.get_position())
        center_far = to_far + vec3(self.__cam.get_position())

        points: list[vec4] = self.calculate_frustum_vertices(rotation, forward_vector, center_near, center_far)

        first = True
        for point in points:
            if first:
                self.__min_x = point.x
                self.__max_x = point.x
                self.__min_y = point.y
                self.__max_y = point.y
                self.__min_z = point.z
                self.__max_z = point.z
                first = False
                continue

            if point.x > self.__max_x:
                self.__max_x = point.x
            elif point.x < self.__min_x:
                self.__min_x = point.x

            if point.y > self.__max_y:
                self.__max_y = point.y
            elif point.y < self.__min_y:
                self.__min_y = point.y

            if point.z > self.__max_z:
                self.__max_z = point.z
            elif point.z < self.__min_z:
                self.__min_z = point.z
        self.__max_z += self.__OFFSET

    def get_center(self) -> vec3:
        """
        Calculates the center of the "view cuboid" in light space first, and then
        Converts this to world space using the inverse light's view matrix.
        :return:
        The center of the "view cuboid" in world space.
        """
        x = (self.__min_x + self.__max_x) / 2.0
        y = (self.__min_y + self.__max_y) / 2.0
        z = (self.__min_z + self.__max_z) / 2.0
        cen = vec4(x, y, z, 1)
        inverted_light = self.__light_view_matrix.inverse()
        center = inverted_light * cen
        return vec3(center.x, center.y, center.z)

    def get_width(self) -> float:
        """
        :return: The width of the "view cuboid" (orthographic projection area).
        """
        return self.__max_x - self.__min_x

    def get_height(self) -> float:
        """
        :return: The height of the "view cuboid" (orthographic projection area).
        """
        return self.__max_y - self.__min_y

    def get_length(self) -> float:
        """
        :return: The height of the "view cuboid" (orthographic projection area).
        """
        return self.__max_z - self.__min_z

    def calculate_frustum_vertices(self, rotation: mat4, forward_vector: vec3, center_near: vec3,
                                   center_far: vec3) -> list[vec4]:
        """
        Calculates the position of the vertex at each corner of the view frustum
        in light space (8 vertices in total, so this returns 8 positions).
        :param rotation: camera's rotation.
        :param forward_vector: the direction that the camera is aiming, and thus the direction of the frustum.
        :param center_near: the center point of the frustum's near plane.
        :param center_far: the center point of the frustum's (possibly adjusted) far plane.
        :return: The positions of the vertices of the frustum in light space.
        """
        up_vector = rotation * self.__UP
        up_vector = vec3(up_vector.x, up_vector.y, up_vector.z)
        right_vector = forward_vector.cross(up_vector)
        down_vector = -up_vector
        left_vector = -right_vector
        far_top = center_far + up_vector * self.__far_height
        far_bottom = center_far + down_vector * self.__far_height
        near_top = center_near + up_vector * self.__near_height
        near_bottom = center_near + down_vector * self.__near_height
        points = [vec4()] * 8
        points[0] = self.calculate_light_space_frustum_corner(far_top, right_vector, self.__far_width)
        points[1] = self.calculate_light_space_frustum_corner(far_top, left_vector, self.__far_width)
        points[2] = self.calculate_light_space_frustum_corner(far_bottom, right_vector, self.__far_width)
        points[3] = self.calculate_light_space_frustum_corner(far_bottom, left_vector, self.__far_width)
        points[4] = self.calculate_light_space_frustum_corner(near_top, right_vector, self.__near_width)
        points[5] = self.calculate_light_space_frustum_corner(near_top, left_vector, self.__near_width)
        points[6] = self.calculate_light_space_frustum_corner(near_bottom, right_vector, self.__near_width)
        points[7] = self.calculate_light_space_frustum_corner(near_bottom, left_vector, self.__near_width)
        return points

    def calculate_light_space_frustum_corner(self, start_point: vec3, direction: vec3, width: float) -> vec4:
        """
        Calculates one of the corner vertices of the view frustum in world space
        and converts it to light space.
        :param start_point: the starting center point on the view frustum.
        :param direction: the direction of the corner from the start point.
        :param width: the distance of the corner from the start point.
        :return: The relevant corner vertex of the view frustum in light space.
        """
        point = start_point + direction * width
        point4f = vec4(point.x, point.y, point.z, 1)
        point4f = self.__light_view_matrix * point4f
        return point4f

    def calculate_camera_rotation_matrix(self) -> mat4:
        """
        :return: The rotation of the camera represented as a matrix.
        """
        rotation = mat4(1)
        rotation = rotation.rotate(radians(-self.__cam.get_yaw()), vec3(0, 1, 0))
        rotation = rotation.rotate(radians(-self.__cam.get_pitch()), vec3(1, 0, 0))
        return rotation

    def calculate_widths_and_heights(self) -> None:
        """
        Calculates the width and height of the near and far planes of the
        camera's view frustum. However, this doesn't have to use the "actual" far
        plane of the view frustum. It can use a shortened view frustum if desired
        by bringing the far-plane closer, which would increase shadow resolution
        but means that distant objects wouldn't cast shadows.
        :return:
        """
        # to prevent circular import
        fov = master_renderer.MasterRenderer.get_fov()
        near_plane = master_renderer.MasterRenderer.get_near_plane()

        self.__far_width = self.__SHADOW_DISTANCE * tan(radians(fov))
        self.__near_width = near_plane * tan(radians(fov))
        self.__far_height = self.__far_width / self.get_aspect_ratio()
        self.__near_height = self.__near_width / self.get_aspect_ratio()

    @staticmethod
    def get_aspect_ratio() -> float:
        """
        :return: The aspect ratio of the display (width:height ratio).
        """
        return DisplayManager.get_width() / DisplayManager.get_height()

Also here are how my matrices are created:

    def update_light_view_matrix(self, direction: vec3, center: vec3) -> None:
        """
        Updates the "view" matrix of the light. This creates a view matrix which
        will line up the direction of the "view cuboid" with the direction of the
        light. The light itself has no position, so the "view" matrix is centered
        at the center of the "view cuboid". The created view matrix determines
        where and how the "view cuboid" is positioned in the world. The size of
        the view cuboid, however, is determined by the projection matrix.
        :param direction: the light direction, and therefore the direction that the "view cuboid" should be pointing.
        :param center: the center of the "view cuboid" in world space.
        :return:
        """
        direction = direction.normalize()
        self.__light_view_matrix = mat4().identity()
        pitch = math.acos(math.sqrt(direction.x**2 + direction.y**2))
        self.__light_view_matrix = self.__light_view_matrix.rotate(pitch, vec3(1, 0, 0))
        yaw = math.degrees(math.atan(direction.x / direction.z))
        yaw = yaw - 180 if direction.z > 0 else yaw
        self.__light_view_matrix = self.__light_view_matrix.rotate(-math.radians(yaw), vec3(0, 1, 0))
        self.__light_view_matrix = self.__light_view_matrix.translate(-center)

    def update_ortho_projection_matrix(self, width: float, height: float, length: float) -> None:
        """
        Creates the orthographic projection matrix. This projection matrix
        basically sets the width, length and height of the "view cuboid", based
        on the values that were calculated in the ShadowBox class.
        :param width: shadow box width.
        :param height: shadow box height.
        :param length: shadow box length.
        :return:
        """
        self.__projection_matrix = mat4()
        self.__projection_matrix[(0, 0)] = 2.0 / width
        self.__projection_matrix[(1, 1)] = 2.0 / height
        self.__projection_matrix[(2, 2)] = -2.0 / length
        self.__projection_matrix[(3, 3)] = 1

The model matrix is definetly correct.

...
self.__projection_view_matrix = self.__projection_matrix * self.__light_view_matrix
...
mvp_matrix = self.__projection_view_matrix * model_matrix

I think I'll add the outputs of the matrix calculations so you can verify them:

projection_matrix:
[   0.0036,    0.0000,    0.0000,    0.0000]
[   0.0000,    0.0069,    0.0000,    0.0000]
[   0.0000,    0.0000,   -0.0128,    0.0000]
[   0.0000,    0.0000,    0.0000,    1.0000]

light_view_matrix:
[  -0.7071,    0.0000,   -0.7071,  -15.1670]
[  -0.3430,    0.8745,    0.3430,   22.1743]
[   0.6183,    0.4851,   -0.6183,   -5.0441]
[   0.0000,    0.0000,    0.0000,    1.0000]

projection_view_matrix:
[  -0.0026,    0.0000,   -0.0026,   -0.0552]
[  -0.0024,    0.0060,    0.0024,    0.1527]
[  -0.0079,   -0.0062,    0.0079,    0.0643]
[   0.0000,    0.0000,    0.0000,    1.0000]

mvp_matrix:
[   1.0000,    0.0000,    0.0000,  575.0000]
[   0.0000,    1.0000,    0.0000,   -0.8157]
[   0.0000,    0.0000,    1.0000,  792.0000]
[   0.0000,    0.0000,    0.0000,    1.0000]

The mvp_matrix for every instance looks something like this as they have no rotation and scaling, just a position.



from OpenGL Depth Map renders objects only 1 unit far away

No comments:

Post a Comment