fortplot_surface_rendering.f90 Source File


Source Code

module fortplot_surface_rendering
    !! Surface rendering module for 3D surface plots
    !!
    !! Single Responsibility: Render filled and wireframe 3D surfaces
    !! Extracted from fortplot_figure_rendering_pipeline for size compliance

    use, intrinsic :: iso_fortran_env, only: wp => real64
    use fortplot_context
    use fortplot_plot_data, only: plot_data_t
    use fortplot_projection, only: project_3d_to_2d, get_default_view_angles
    use fortplot_colormap, only: colormap_value_to_color
    implicit none

    private
    public :: render_surface_plot

contains

    subroutine render_surface_plot(backend, plot, x_min_t, x_max_t, y_min_t, &
                                   y_max_t, xscale, yscale, symlog_threshold)
        !! Render a 3D surface plot using wireframe or filled representation
        class(plot_context), intent(inout) :: backend
        type(plot_data_t), intent(in) :: plot
        real(wp), intent(in) :: x_min_t, x_max_t, y_min_t, y_max_t
        character(len=*), intent(in) :: xscale, yscale
        real(wp), intent(in) :: symlog_threshold

        integer :: nx, ny
        real(wp) :: x_min, x_max, y_min, y_max, z_min, z_max
        real(wp) :: range_x, range_y, range_z
        real(wp) :: azim, elev, dist
        real(wp) :: x_corners(8), y_corners(8), z_corners(8)
        real(wp) :: x_proj_corners(8), y_proj_corners(8)
        real(wp) :: proj_x_min, proj_x_max, proj_y_min, proj_y_max
        real(wp) :: denom_x, denom_y
        logical :: transposed
        character(len=20) :: cmap

        associate(unused_xt => x_min_t, unused_xx => x_max_t, &
                  unused_yt => y_min_t, unused_yx => y_max_t)
        end associate
        associate(unused_xs => xscale, unused_ys => yscale, &
                  unused_st => symlog_threshold)
        end associate

        if (.not. allocated(plot%x_grid)) return
        if (.not. allocated(plot%y_grid)) return
        if (.not. allocated(plot%z_grid)) return

        nx = size(plot%x_grid)
        ny = size(plot%y_grid)
        if (nx < 2 .or. ny < 2) return
        if (size(plot%z_grid, 1) == ny .and. size(plot%z_grid, 2) == nx) then
            transposed = .false.
        else if (size(plot%z_grid, 1) == nx .and. size(plot%z_grid, 2) == ny) then
            transposed = .true.
        else
            return
        end if

        x_min = minval(plot%x_grid)
        x_max = maxval(plot%x_grid)
        y_min = minval(plot%y_grid)
        y_max = maxval(plot%y_grid)
        z_min = minval(plot%z_grid)
        z_max = maxval(plot%z_grid)

        range_x = max(1.0e-9_wp, x_max - x_min)
        range_y = max(1.0e-9_wp, y_max - y_min)
        range_z = max(1.0e-9_wp, z_max - z_min)

        call get_default_view_angles(azim, elev, dist)

        x_corners = [0.0_wp, 1.0_wp, 1.0_wp, 0.0_wp, 0.0_wp, 1.0_wp, 1.0_wp, &
                     0.0_wp]
        y_corners = [0.0_wp, 0.0_wp, 1.0_wp, 1.0_wp, 0.0_wp, 0.0_wp, 1.0_wp, &
                     1.0_wp]
        z_corners = [0.0_wp, 0.0_wp, 0.0_wp, 0.0_wp, 1.0_wp, 1.0_wp, 1.0_wp, &
                     1.0_wp]
        call project_3d_to_2d(x_corners, y_corners, z_corners, azim, elev, &
                              dist, x_proj_corners, y_proj_corners)

        proj_x_min = minval(x_proj_corners)
        proj_x_max = maxval(x_proj_corners)
        proj_y_min = minval(y_proj_corners)
        proj_y_max = maxval(y_proj_corners)
        denom_x = max(1.0e-9_wp, proj_x_max - proj_x_min)
        denom_y = max(1.0e-9_wp, proj_y_max - proj_y_min)

        cmap = 'viridis'
        if (allocated(plot%surface_colormap)) then
            if (len_trim(plot%surface_colormap) > 0) then
                cmap = plot%surface_colormap
            end if
        end if

        if (plot%surface_filled) then
            call render_filled_surface(backend, plot, nx, ny, transposed, &
                                       x_min, y_min, z_min, range_x, range_y, &
                                       range_z, azim, elev, dist, proj_x_min, &
                                       proj_y_min, denom_x, denom_y, cmap)
        end if

        call render_wireframe_surface(backend, plot, nx, ny, transposed, &
                                      x_min, y_min, z_min, range_x, range_y, &
                                      range_z, azim, elev, dist, proj_x_min, &
                                      proj_y_min, denom_x, denom_y)
    end subroutine render_surface_plot

    subroutine render_filled_surface(backend, plot, nx, ny, transposed, &
                                     x_min, y_min, z_min, range_x, range_y, &
                                     range_z, azim, elev, dist, proj_x_min, &
                                     proj_y_min, denom_x, denom_y, cmap)
        !! Render filled surface quads using painters algorithm
        class(plot_context), intent(inout) :: backend
        type(plot_data_t), intent(in) :: plot
        integer, intent(in) :: nx, ny
        logical, intent(in) :: transposed
        real(wp), intent(in) :: x_min, y_min, z_min
        real(wp), intent(in) :: range_x, range_y, range_z
        real(wp), intent(in) :: azim, elev, dist
        real(wp), intent(in) :: proj_x_min, proj_y_min, denom_x, denom_y
        character(len=*), intent(in) :: cmap

        integer :: i, j, k, n_quads
        integer, allocatable :: sorted_idx(:)
        real(wp), allocatable :: quad_depth(:)
        real(wp) :: z00, z10, z01, z11, z_avg
        real(wp) :: x_norm(4), y_norm(4), z_norm(4)
        real(wp) :: x_proj(4), y_proj(4), x_final(4), y_final(4)
        real(wp) :: quad_color(3)

        n_quads = (nx - 1) * (ny - 1)
        if (n_quads <= 0) return

        allocate(quad_depth(n_quads), sorted_idx(n_quads))

        k = 0
        do j = 1, ny - 1
            do i = 1, nx - 1
                k = k + 1
                if (.not. transposed) then
                    z00 = plot%z_grid(j, i)
                    z10 = plot%z_grid(j, i + 1)
                    z01 = plot%z_grid(j + 1, i)
                    z11 = plot%z_grid(j + 1, i + 1)
                else
                    z00 = plot%z_grid(i, j)
                    z10 = plot%z_grid(i + 1, j)
                    z01 = plot%z_grid(i, j + 1)
                    z11 = plot%z_grid(i + 1, j + 1)
                end if
                quad_depth(k) = (z00 + z10 + z01 + z11) / 4.0_wp
            end do
        end do

        call sort_indices_by_depth(quad_depth, sorted_idx, n_quads)

        do k = 1, n_quads
            call index_to_ij(sorted_idx(k), nx - 1, i, j)

            if (.not. transposed) then
                z00 = plot%z_grid(j, i)
                z10 = plot%z_grid(j, i + 1)
                z01 = plot%z_grid(j + 1, i)
                z11 = plot%z_grid(j + 1, i + 1)
            else
                z00 = plot%z_grid(i, j)
                z10 = plot%z_grid(i + 1, j)
                z01 = plot%z_grid(i, j + 1)
                z11 = plot%z_grid(i + 1, j + 1)
            end if
            z_avg = (z00 + z10 + z01 + z11) / 4.0_wp

            x_norm(1) = (plot%x_grid(i) - x_min) / range_x
            x_norm(2) = (plot%x_grid(i + 1) - x_min) / range_x
            x_norm(3) = (plot%x_grid(i + 1) - x_min) / range_x
            x_norm(4) = (plot%x_grid(i) - x_min) / range_x

            y_norm(1) = (plot%y_grid(j) - y_min) / range_y
            y_norm(2) = (plot%y_grid(j) - y_min) / range_y
            y_norm(3) = (plot%y_grid(j + 1) - y_min) / range_y
            y_norm(4) = (plot%y_grid(j + 1) - y_min) / range_y

            z_norm(1) = (z00 - z_min) / range_z
            z_norm(2) = (z10 - z_min) / range_z
            z_norm(3) = (z11 - z_min) / range_z
            z_norm(4) = (z01 - z_min) / range_z

            call project_3d_to_2d(x_norm, y_norm, z_norm, azim, elev, dist, &
                                  x_proj, y_proj)

            x_final(1) = x_min + (x_proj(1) - proj_x_min) / denom_x * range_x
            x_final(2) = x_min + (x_proj(2) - proj_x_min) / denom_x * range_x
            x_final(3) = x_min + (x_proj(3) - proj_x_min) / denom_x * range_x
            x_final(4) = x_min + (x_proj(4) - proj_x_min) / denom_x * range_x

            y_final(1) = y_min + (y_proj(1) - proj_y_min) / denom_y * range_y
            y_final(2) = y_min + (y_proj(2) - proj_y_min) / denom_y * range_y
            y_final(3) = y_min + (y_proj(3) - proj_y_min) / denom_y * range_y
            y_final(4) = y_min + (y_proj(4) - proj_y_min) / denom_y * range_y

            call colormap_value_to_color(z_avg, z_min, z_min + range_z, cmap, &
                                         quad_color)

            if (plot%surface_alpha < 1.0_wp) then
                quad_color = plot%surface_alpha * quad_color + &
                            (1.0_wp - plot%surface_alpha) * 1.0_wp
            end if

            call backend%color(quad_color(1), quad_color(2), quad_color(3))
            call backend%fill_quad(x_final, y_final)
        end do

        deallocate(quad_depth, sorted_idx)
    end subroutine render_filled_surface

    subroutine render_wireframe_surface(backend, plot, nx, ny, transposed, &
                                        x_min, y_min, z_min, range_x, range_y, &
                                        range_z, azim, elev, dist, proj_x_min, &
                                        proj_y_min, denom_x, denom_y)
        !! Render wireframe lines for surface plot
        class(plot_context), intent(inout) :: backend
        type(plot_data_t), intent(in) :: plot
        integer, intent(in) :: nx, ny
        logical, intent(in) :: transposed
        real(wp), intent(in) :: x_min, y_min, z_min
        real(wp), intent(in) :: range_x, range_y, range_z
        real(wp), intent(in) :: azim, elev, dist
        real(wp), intent(in) :: proj_x_min, proj_y_min, denom_x, denom_y

        integer :: i, j, m, max_points
        real(wp), allocatable :: x_vals(:), y_vals(:), z_vals(:)
        real(wp), allocatable :: x_norm(:), y_norm(:), z_norm(:)
        real(wp), allocatable :: x_proj(:), y_proj(:)
        real(wp), allocatable :: x_final(:), y_final(:)
        real(wp) :: line_color(3)

        max_points = max(nx, ny)
        allocate(x_vals(max_points), y_vals(max_points), z_vals(max_points))
        allocate(x_norm(max_points), y_norm(max_points), z_norm(max_points))
        allocate(x_proj(max_points), y_proj(max_points))
        allocate(x_final(max_points), y_final(max_points))

        line_color = plot%surface_edgecolor
        if (plot%surface_alpha < 1.0_wp) then
            line_color = plot%surface_alpha * line_color + &
                        (1.0_wp - plot%surface_alpha) * 1.0_wp
        end if
        call backend%color(line_color(1), line_color(2), line_color(3))
        call backend%set_line_style('-')
        call backend%set_line_width(plot%surface_linewidth)

        do j = 1, ny
            m = nx
            x_vals(1:m) = plot%x_grid
            y_vals(1:m) = plot%y_grid(j)
            if (.not. transposed) then
                z_vals(1:m) = plot%z_grid(j, :)
            else
                z_vals(1:m) = plot%z_grid(:, j)
            end if

            x_norm(1:m) = (x_vals(1:m) - x_min) / range_x
            y_norm(1:m) = (y_vals(1:m) - y_min) / range_y
            z_norm(1:m) = (z_vals(1:m) - z_min) / range_z

            call project_3d_to_2d(x_norm(1:m), y_norm(1:m), z_norm(1:m), &
                                  azim, elev, dist, x_proj(1:m), y_proj(1:m))

            do i = 1, m
                x_final(i) = x_min + (x_proj(i) - proj_x_min) / denom_x * &
                             range_x
                y_final(i) = y_min + (y_proj(i) - proj_y_min) / denom_y * &
                             range_y
            end do

            do i = 1, m - 1
                call backend%line(x_final(i), y_final(i), x_final(i+1), &
                                  y_final(i+1))
            end do
        end do

        do i = 1, nx
            m = ny
            x_vals(1:m) = plot%x_grid(i)
            y_vals(1:m) = plot%y_grid
            if (.not. transposed) then
                z_vals(1:m) = plot%z_grid(:, i)
            else
                z_vals(1:m) = plot%z_grid(i, :)
            end if

            x_norm(1:m) = (x_vals(1:m) - x_min) / range_x
            y_norm(1:m) = (y_vals(1:m) - y_min) / range_y
            z_norm(1:m) = (z_vals(1:m) - z_min) / range_z

            call project_3d_to_2d(x_norm(1:m), y_norm(1:m), z_norm(1:m), &
                                  azim, elev, dist, x_proj(1:m), y_proj(1:m))

            do j = 1, m
                x_final(j) = x_min + (x_proj(j) - proj_x_min) / denom_x * &
                             range_x
                y_final(j) = y_min + (y_proj(j) - proj_y_min) / denom_y * &
                             range_y
            end do

            do j = 1, m - 1
                call backend%line(x_final(j), y_final(j), x_final(j+1), &
                                  y_final(j+1))
            end do
        end do

        deallocate(x_vals, y_vals, z_vals, x_norm, y_norm, z_norm, x_proj, &
                   y_proj, x_final, y_final)
    end subroutine render_wireframe_surface

    subroutine sort_indices_by_depth(depths, indices, n)
        !! Sort indices by depth (back to front for painters algorithm)
        real(wp), intent(in) :: depths(:)
        integer, intent(out) :: indices(:)
        integer, intent(in) :: n

        integer :: i, j, min_idx, temp_idx
        real(wp), allocatable :: temp_depths(:)

        allocate(temp_depths(n))
        temp_depths = depths(1:n)

        do i = 1, n
            indices(i) = i
        end do

        do i = 1, n - 1
            min_idx = i
            do j = i + 1, n
                if (temp_depths(j) < temp_depths(min_idx)) then
                    min_idx = j
                end if
            end do
            if (min_idx /= i) then
                temp_idx = indices(i)
                indices(i) = indices(min_idx)
                indices(min_idx) = temp_idx

                temp_depths(min_idx) = temp_depths(i)
            end if
        end do

        deallocate(temp_depths)
    end subroutine sort_indices_by_depth

    subroutine index_to_ij(idx, row_size, i, j)
        !! Convert linear index to i,j indices (1-based)
        integer, intent(in) :: idx, row_size
        integer, intent(out) :: i, j

        j = (idx - 1) / row_size + 1
        i = mod(idx - 1, row_size) + 1
    end subroutine index_to_ij

end module fortplot_surface_rendering