pysdic.create_triangle_3_heightmap#

create_triangle_3_heightmap(height_function=<function <lambda>>, frame=None, x_bounds=(-1.0, 1.0), y_bounds=(-1.0, 1.0), n_x=10, n_y=10, first_diagonal=True, direct=True, uv_layout=0)[source]#

Create a 3D Mesh XY-plane mesh with variable height defined by a surface function.

The surface is defined by a function that takes two arguments (x and y) and returns a scalar height z. The returned value is interpreted as the vertical position of the surface at that point.

The frame parameter defines the orientation and the position of the mesh in 3D space. The (x, y) grid is centered on the frame origin, lying in the local XY plane. The height (z) is applied along the local Z-axis of the frame.

The x_bounds and y_bounds parameters define the rectangular domain over which the mesh is generated. n_x and n_y determine the number of vertices along the x and y directions, respectively. Nodes are uniformly distributed across both directions.

Note

  • n_x and n_y refer to the number of vertices, not segments.

  • The height function must return a finite scalar value for every (x, y) in the specified domain.

For example, the following code generates a sinusoidal surface mesh centered on the world origin:

from pysdic import create_triangle_3_heightmap
import numpy as np

surface_mesh = create_triangle_3_heightmap(
    height_function=lambda x, y: 0.5 * np.sin(np.pi * x) * np.cos(np.pi * y),
    x_bounds=(-1.0, 1.0),
    y_bounds=(-1.0, 1.0),
    n_x=50,
    n_y=50,
)

surface_mesh.visualize()
../_images/create_triangle_3_heightmap_example.png

Sinusoidal height map over a square domain centered at the origin.#

Nodes are ordered first along the x direction, then along the y direction. So the vertex at y index i_Y and x index i_X (both starting from 0) is located at:

mesh.vertices[i_Y * n_x + i_X, :]

Each quadrilateral face is defined by the vertices:

  • \((i_X, i_Y)\)

  • \((i_X + 1, i_Y)\)

  • \((i_X + 1, i_Y + 1)\)

  • \((i_X, i_Y + 1)\)

This quadrilateral is split into two triangles.

See also

  • Mesh for more information on how to visualize and manipulate the mesh.

  • Artezaru/py3dframe for details on the Frame class.

Parameters:
  • height_function (Callable[[float, float], float]) – A function that takes x and y coordinates and returns the corresponding height (z).

  • frame (Frame, optional) – The reference frame for the mesh. Defaults to the canonical frame.

  • x_bounds (Tuple[float, float], optional) – The lower and upper bounds for the x coordinate. Default is (-1.0, 1.0).

  • y_bounds (Tuple[float, float], optional) – The lower and upper bounds for the y coordinate. Default is (-1.0, 1.0).

  • n_x (int, optional) – Number of vertices along the x direction. Must be more than 1. Default is 10.

  • n_y (int, optional) – Number of vertices along the y direction. Must be more than 1. Default is 10.

  • first_diagonal (bool, optional) – If True, the quad is split along the first diagonal (bottom-left to top-right). Default is True.

  • direct (bool, optional) – If True, triangle vertices are ordered counterclockwise for an observer looking from above. Default is True.

  • uv_layout (int, optional) – The UV mapping strategy. Default is 0.

Returns:

The generated surface mesh as a Mesh object.

Return type:

Mesh

Geometry of the mesh#

Diagonal selection#

According to the first_diagonal parameter, each quadrilateral face is split into two triangles as follows:

  • If first_diagonal is True:

    • Triangle 1: \((i_X, i_Y)\), \((i_X + 1, i_Y)\), \((i_X + 1, i_Y + 1)\)

    • Triangle 2: \((i_X, i_Y)\), \((i_X + 1, i_Y + 1)\), \((i_X, i_Y + 1)\)

  • If first_diagonal is False:

    • Triangle 1: \((i_X, i_Y)\), \((i_X + 1, i_Y)\), \((i_X, i_Y + 1)\)

    • Triangle 2: \((i_X, i_Y + 1)\), \((i_X + 1, i_Y)\), \((i_X + 1, i_Y + 1)\)

../_images/create_triangle_3_heightmap_diagonal.png

Diagonal selection for splitting quadrilaterals into triangles.#

Triangle orientation#

  • If direct is True, triangles are oriented counterclockwise for an observer looking from above (See the Diagonal selection section).

  • If direct is False, triangles are oriented clockwise for an observer looking from above. Switch the order of the last two vertices in each triangle defined above.

UV Mapping#

The UV coordinates are generated based on the vertex positions in the mesh and uniformly distributed in the range [0, 1] for the OpenGL texture mapping convention (See https://learnopengl.com/Getting-started/Textures). Several UV mapping strategies are available and synthesized in the uv_layout parameter.

An image is defined by its four corners:

  • Lower-left corner : pixel with array coordinates image[height-1, 0] but OpenGL convention is (0,0) at lower-left

  • Upper-left corner : pixel with array coordinates image[0, 0] but OpenGL convention is (0,1) at upper-left

  • Lower-right corner : pixel with array coordinates image[height-1, width-1] but OpenGL convention is (1,0) at lower-right

  • Upper-right corner : pixel with array coordinates image[0, width-1] but OpenGL convention is (1,1) at upper-right

The following options are available for uv_layout and their corresponding vertex mapping:

uv_layout

Vertex lower-left corner

Vertex upper-left corner

Vertex lower-right corner

Vertex upper-right corner

0

(0, 0)

(0, n_y-1)

(n_x-1, 0)

(n_x-1, n_y-1)

1

(0, 0)

(n_x-1, 0)

(0, n_y-1)

(n_x-1, n_y-1)

2

(n_x-1, 0)

(0, 0)

(n_x-1, n_y-1)

(0, n_y-1)

3

(0, n_y-1)

(0, 0)

(n_x-1, n_y-1)

(n_x-1, 0)

4

(0, n_y-1)

(n_x-1, n_y-1)

(0, 0)

(n_x-1, 0)

5

(n_x-1, 0)

(n_x-1, n_y-1)

(0, 0)

(0, n_y-1)

6

(n_x-1, n_y-1)

(0, n_y-1)

(n_x-1, 0)

(0, 0)

7

(n_x-1, n_y-1)

(n_x-1, 0)

(0, n_y-1)

(0, 0)

The table above gives for the 4 corners of a image the corresponding vertex in the mesh.

../_images/create_triangle_3_heightmap_uv_layout.png

UV mapping strategies for different uv_layout options.#

To check the UV mapping, you can use the following code:

import numpy as np
from pysdic import create_triangle_3_heightmap
import cv2

surface_mesh = create_triangle_3_heightmap(
    height_function=lambda x, y: 0.5 * np.sin(np.pi * x) * np.cos(np.pi * y),
    x_bounds=(-1.0, 1.0),
    y_bounds=(-1.0, 1.0),
    n_x=50,
    n_y=50,
    uv_layout=3,
)

image = cv2.imread("lena_image.png")
surface_mesh.visualize_texture(image)
../_images/create_triangle_3_heightmap_lena_texture.png

UV mapping of the Lena image on a sinusoidal height map.#