Channel Surfaces and Focal Surfaces

../../../_images/channel_surface.jpg

In this example we construct channel surfaces and focal surfaces. We start with a discrete one-parameter family of spheres. The channel surface can be obtained by choosing a circle on the first, or any, sphere and iteratively applying sphere inversions in the mid-spheres of anti-similitude. This will construct a discrete surface with planar, even circular faces. The circles can be used to define normal lines for the faces, the normal lines being orthogonal to the face and passing through the circles center. From the obtained normal line congruence we construct the two discrete focal surfaces by intersecting successive normal lines for each parameter direction. One of the focal surfaces degenerates to a discrete curve, which always is the case for channel surfaces. The main part of the example consists of a function taking as an argument a name for an example (smooth or discrete). We will go through the discrete example in this guide.

Download the full script here: channel_surfaces.py.

Very similar methods for sphere inversions and envelopes (in 2 dimensions) were introduced in the example Envelopes and Orthogonal Trajectories of Families of Circles. In some cases we will just refer to this example and refrain from detailed explanations here.

Setup

We start off by importing the necessary libraries, clearing the scene and initializing the projective and the Moebius geometric model of three dimensional Euclidean space.

# Import necessary libraries
import numpy as np

import ddg
from ddg.geometry.euclidean_models import moebius_to_projective, projective_to_moebius
from ddg.geometry.intersection import intersect, join
from ddg.geometry.subspaces import (
    Point,
    orthonormalize_and_center_subspace,
    subspace_from_affine_points,
)
from ddg.visualization.blender.material import material

# Clear the existing objects in the Blender scene
ddg.visualization.blender.scene.clear()
ddg.visualization.blender.material.clear()


euc = ddg.geometry.euclidean(3)
mob = ddg.geometry.euclidean_models.MoebiusModel(3)

The One-Parameter Family of Spheres

The main script starts by defining a lot of helper functions. In the end the examples will be generated only by suitable composition of these functions. A lot of the functions are straight forward, and we will not explicitly include all of them here. Furthermore, a lot of them have already been defined in the example on Envelopes and Orthogonal Trajectories of Families of Circles.

Now lets get started by defining a discrete curve in \(\mathbb{R}^3\), radii of spheres associated to the vertices and some initial values.

    elif example_name == "discrete":
        # Samplings in two parameter directions
        sampling_curve = 5
        v_circle_sampling = 20

        # Parameterization of a 2d parabola

        def parabola_parameterization(u):
            return 5 * np.array([u, -(u**2) / 2])

        snet_2d = ddg.nets.net.SmoothNet(
            parabola_parameterization, [[0, 1 / 2 * np.pi]]
        )
        snet = ddg.nets.utils.embed(snet_2d)
        dnet = ddg.sample_smooth_net(snet, sampling=[sampling_curve, "t"])

        # Radii of the spheres associated to the vertices
        radii = np.linspace(1, 2, sampling_curve)

        # Start of discrete envelope. The first circle is the intersection of this plane
        # with the first sphere
        plane = ddg.geometry.subspaces.Subspace(
            [-0.2, 1, 0, 1], [-0.2, 1, 1, 1], [-0.2, 0, 1, 1]
        )

        # Visualization domain of the focal surface
        domain_focal_surf = ddg.nets.DiscreteDomain([[0, 2], [-4, 3]])

        # Visualization
        dnet_bevel = 0.04
        camera_location = (-13, -5, 8)
        camera_look_at_point = (5, -2, 0)
    else:
        raise ValueError(
            "The only examples implemented are called 'smooth' and 'discrete'."
        )

We can define the one-parameter family of spheres as a discrete function and further, define their lift to homogeneous coordinates in \(\mathbb{R}^{4, 1}\) of the form \(\frac{1}{r}(c, \frac{1}{2}(||c||^2 - r^2 + 1) , \frac{1}{2}(||c||^2 - r^2 - 1))\).

    ###############################################
    # Spheres
    ###############################################

    # Euclidean ###################################
    def e_spheres_fct(i):
        return euc.sphere_from_affine_point_and_normals(dnet.fct(i), radii[i])

    # Moebius #####################################
    m_homogeneous_points_fct = homogeneous_lift_of_spheres_fct(
        e_spheres_fct
    )  # Homogeneous Points in R^{4, 1}
../../../_images/01_sphere_family_combined.jpg

The Channel Surface

We can use the homogeneous coordinates to find the mid-spheres, in particular the spheres of anti-similitude (see Envelopes and Orthogonal Trajectories of Families of Circles).

    ###############################################
    # Spheres of anti-similitude
    ###############################################

    # Moebius #####################################
    m_points_of_anti_similitude_fct = (
        m_points_of_anti_similitude_from_homogeneous_lifts(m_homogeneous_points_fct)
    )  # Points in M+
    m_spheres_of_anti_similitude_fct = m_points_to_spheres(
        m_points_of_anti_similitude_fct
    )  # Quadrics in M

    # Euclidean ####################################
    e_spheres_of_anti_similitude_fct = project_function_of_objects_down(
        m_spheres_of_anti_similitude_fct
    )  # Spheres in R3
    ###############################################
    # Envelope (as sphere inversions in spheres of anti-similitude)
    ###############################################

    # Intersecting the first sphere with the plane (given in the initial values
    # of the example) determines a starting circle. A sampling yields a list of points.
    # Iterative sphere inversions in the mid-spheres of anti-similitude
    # form the discrete channel surface.

    # Starting circle ##############################
    circle = ddg.geometry.intersection.intersect(
        plane, euc.sphere_to_quadric(e_spheres_fct(0))
    )  # Conic in R3
    circle_dnet = ddg.sample_smooth_net(
        ddg.to_smooth_net(circle), sampling=[v_circle_sampling, "t"]
    )  # DiscreteNet in R3
    cirlce_pts = [
        circle_dnet.fct(k) for k, in circle_dnet.domain.traverser
    ]  # List of affine coordinates
    m_starting_pts = [
        projective_to_moebius(subspace_from_affine_points(p)) for p in cirlce_pts
    ]  # List of points in M

    # Moebius #####################################
    m_surface_points_fct = m_iterative_sphere_inversions(
        m_points_of_anti_similitude_fct, m_starting_pts
    )  # Two-parameter family of points in M

    # Euclidean ####################################
    e_surface_points_fct = function_projective_to_affine(
        project_function_of_objects_down(m_surface_points_fct)
    )  # Two-parameter family of affine coordinates

    def channel_surface_parameterization(i, j):
        return e_surface_points_fct(i)[j]

../../../_images/02_channel_surface_combined.jpg

The visualization is also done by using helper functions and can be found as the last section of this guide.


Circumcircles and Normal Lines

    ###############################################
    # Circumcircles of the channel surface
    ###############################################

    # Channel surfaces are circular, for each face we determine the
    # circle through its four vertices. It suffices to determine the circle through
    # three of its vertices.
    def face_circles_data(i, j):
        """
        Returns the center, radius, and normal of the
        circumcircle of the face [(i, j), (i+1, j), (i+1, j+1), (i, j+1)].
        """
        points = [
            channel_surface_parameterization(i, j),
            channel_surface_parameterization(i + 1, j),
            channel_surface_parameterization(i, j + 1),
        ]
        c, r, n = ddg.math.euclidean.circle_through_three_points(*points)
        return c, r, n

    def face_circles(i, j):
        """
        Returns the circumcircle of the face [(i, j), (i+1, j), (i+1, j+1), (i, j+1)].
        """
        return euc.sphere_from_affine_point_and_normals(*face_circles_data(i, j))

../../../_images/03_circular_faces_combined.jpg

    ##############################################
    # Normal line congruence
    ##############################################
    # From the same data we can compute face normal lines,
    # orthogonal to the faces and passing through the centers
    # of the circumcircles.
    def normal_line_congruence(i, j):
        """
        Returns the normal line to each face passing through the circumcircles center.
        """
        c, r, n = face_circles_data(i, j)
        line = ddg.geometry.subspaces.subspace_from_affine_points_and_directions(c, n)
        return line

For each parameter line in the first parameter direction the corresponding circles lie on a sphere, the curvature sphere (nicely to see in the right picture below). All the normal lines corresponding to this family in the first parameter direction pass through the spheres center.

../../../_images/04_normal_line_congruence_combined.jpg

The Focal Surfaces

    ##############################################
    # Focal surfaces
    ##############################################

    # Neighbouring normal lines intersect. The intersection points
    # determine two new surfaces, the focal surfaces.
    # One of these surfaces degenerates to a discrete curve. We compute the other.

    def focal_surf_i(i, j):
        """
        Returns the intersection points of successive normal lines in the fist
        parameter direction.
        """
        return ddg.geometry.intersection.intersect(
            normal_line_congruence(i, j), normal_line_congruence(i + 1, j)
        ).affine_point

../../../_images/05_focal_surface_combined.jpg

Visualization

With all this setup done, we can now visualize this in Blender. We start by defining some colors and helper functions. For some functions the discrete domain may be clipped at the start or at the end. For this we also use a helper function.

################################################
# Visualization setup
################################################
orange = material("orange", (0.8, 0.1, 0.036), 0, 0)
gray = material("gray", (0.1, 0.1, 0.1), 0, 0, 1)
black = material("black", (0, 0, 0), 0, 0, 1)
lightgray = material("lightgray", (0.7, 0.7, 0.7), 0, 0, 1)
transparent_sphere = ddg.visualization.blender.material.material(
    "transparent_sphere", (0.018, 0.313, 0.656), 0.5, 0.5, alpha=0.8
)

bevel = 0.03
sampling = [0.1, 100, "c"]


def visualize_spheres(g, domain, **kwargs):
    return [
        ddg.to_blender_object_helper(
            g(i),
            sampling=sampling,
            name=f"sphere_{i}",
            **kwargs,
        )
        for i, in domain.traverser
    ]


def visualize_lines(g, domain, line_domain, **kwargs):
    return [
        ddg.to_blender_object_helper(
            g(i, j),
            domain=line_domain,
            sampling=sampling,
            curve_properties={"bevel_depth": bevel},
            name=f"line_{i}",
            **kwargs,
        )
        for i, j in domain.traverser
    ]


def visualize_circles(g, domain, **kwargs):
    return [
        ddg.to_blender_object_helper(
            g(i, j),
            sampling=sampling,
            curve_properties={"bevel_depth": bevel},
            name=f"circle_{i}",
            **kwargs,
        )
        for i, j in domain.traverser
    ]


def wire_bobj_to_curve(bobj, bevel_depth=0.025):
    # Choose the object mode context
    with ddg.visualization.blender.context.mode(bobj, mode="OBJECT"):
        # Save the material
        mat = bobj.active_material
        # Select the object
        bpy.ops.object.select_all(action="DESELECT")
        bobj.select_set(True)
        # Convert it to a curve
        bpy.ops.object.convert(target="CURVE")
        # Unselect the object
        bobj.select_set(False)
        # Set its bevel_depth and its material
        bobj.data.bevel_depth = bevel_depth
        ddg.visualization.blender.material.set_material(bobj, mat)
        return bobj


For each example (“smooth”/”discrete”) we add a set of collections …

    col = ddg.visualization.blender.collection.collection(
        f"Channel_surface_{example_name}",
        children=[
            f"spheres_{example_name}",
            f"envelope_{example_name}",
            f"circumscribed_circles_{example_name}",
            f"normal_lines_{example_name}",
            f"focal_surfaces_{example_name}",
        ],
    )

… and visualize all the objects.

    ###############################################
    # Channel surface
    ###############################################

    # Discrete curve
    ddg.to_blender_object_helper(
        dnet, curve_properties={"bevel_depth": dnet_bevel}, material=black
    )
    # Spheres
    visualize_spheres(
        e_spheres_fct,
        domain=dnet.domain,
        material=transparent_sphere,
        collection=col.children[0],
    )

    # Channel surface
    channel_surface_domain = ddg.nets.DiscreteDomain(
        [dnet.domain[0], [0, v_circle_sampling - 1, True]]
    )
    channel_bobj = ddg.to_blender_object_helper(
        ddg.nets.DiscreteNet(
            channel_surface_parameterization,
            domain=channel_surface_domain,
        ),
        material=lightgray,
        only_wire=True,
        collection=col.children[1],
    )

    wire_bobj_to_curve(channel_bobj)

    ###############################################
    # Circumscribed circles
    ###############################################
    circumcircles_domain = ddg.nets.DiscreteDomain(
        [[dnet.domain[0][0], dnet.domain[0][1] - 1], [0, v_circle_sampling - 2]]
    )
    visualize_circles(
        face_circles,
        domain=circumcircles_domain,
        material=gray,
        collection=col.children[2],
    )

    ###############################################
    # Normal line congruence
    ###############################################

    visualize_lines(
        normal_line_congruence,
        domain=circumcircles_domain,
        line_domain=[[-2, 2]],
        material=orange,
        collection=col.children[3],
    )

    ###############################################
    # Focal surface
    ###############################################

    visualize_lines(
        normal_line_congruence,
        domain=domain_focal_surf,
        line_domain=[[0, 100]],
        collection=col.children[4],
        material=orange,
    )

    focal_dnet = ddg.DiscreteNet(focal_surf_i, domain=domain_focal_surf)
    ddg.to_blender_object_helper(focal_dnet, material=gray, collection=col.children[4])
    focal_wire = ddg.to_blender_object_helper(
        focal_dnet, only_wire=True, material=gray, collection=col.children[4]
    )
    wire_bobj_to_curve(focal_wire, bevel_depth=0.04)

The script further includes adding lights and cameras and setting up the scene for rendering.