Texture mapping is a powerful way to add deformations to the surface of your objects. This is one of the advanced techniques to complete within the SDK.
During this tutorial we will:
(1) Introduce the operators used to map a texture to a surface
(2) Show how to build a shape graph using our suite of graph building functions.
The graph
The Metafold backend create shapes by consuming directed acyclic graphs where each node represents a series of mathematical computations or the initialization of source values. We can build graphs very quickly using the SDK. See here for details on current operations.
Texture mapping requires the use of 3 operators:
LoadVolume
is responsible for bringing the data that defines the texture into the graph.MapTexture
creates a scalar field defined at every grid point. This field is generated by mapping the original input points x, y, z to the heightmap space u, v via the different mapping types:"Box"
,"Cylinder"
,"Plane"
,"Sphere"
. The values of the field at each point are the interpolated values of the heightmap at u, v.LinearFilter
scales and adds the perturbation field created to the original implicit field of our shape.
The result is a new way to add topological complexity to your shape.
Process
We want to map the following texture onto a hand brace mold.
The first step is up upload the image and convert it to a heightmap using the convert_image_to_heightmap()
job. This jobs takes in any coloured image, converts it to grayscale and normalizes the resulting values to the range [0, 1], where black receives a value of 0, and white receives 1. The output is written to a binary file of 32-bit floats.
from metafold import MetafoldClient
from metafold.func_type import VolumeAsset, JSONEvaluator
from metafold.func import *
from metafold.util import xform
metafold = MetafoldClient(access_token="...", project_id="...")
image_filename = "octagon.jpg"
if existing := metafold.assets.list(q=f"filename:{image_filename}"):
image = metafold.assets.update(existing[0].id, image_filename)
else:
image = metafold.assets.create(image_filename)
heightmap_job = metafold.jobs.run(
"convert_image_to_heightmap",
{
image_filename
}
)
heightmap = heightmap_job.assets[0]
dim_x, dim_y = heightmap_job.meta["size"]
Stored in the metadata of the job are the dimensions of the image. We store these for later use.
Now that we have out heightmap we can apply it to a shape.
# Assume we have defined our shape previously...
source = ...
brace = ...
We take advantage of the LoadVolume
operator to take our new heightmap asset and bring it into our graph for evaluation. The resolution parameter must be set to the size of the image. Since this is a 2D texture we set the resolution in z to be 1.
volume_asset = VolumeAsset()
volume_asset["path"] = heightmap.filename
load_volume = LoadVolume(volume_asset, {"resolution": [dim_x, dim_y, 1]})
We now use the MapTexturePrimitive
operator. We use the xform
utility function to define a affine transform to the underlying cylinder which we are mapping to. We then use scale
to control the size of the mapping in the radial and angular coordinates.
# We first define a transform we want to apply to the underling primtive.
xform_ = xform(rotation=[44.0, 0.0, 70.0], translation=[90.0, 0.0, 0.0])
pertubation_field = MapTexturePrimitive(
source,
load_volume,
{
"shape_type": "Cylinder",
"shape_xform": xform_,
"scale": [0.2, 2],
}
)
To now apply the deformations to our shape, we use the LinearFilter
operator. We port the perturbation field through inputb
of this operator. Hence we can control the level of deformation to the 0-level set with the scale_b
parameter.
punctured_brace = LinearFilter(shape, pertubation_field, {"scale_b": 2.0})
# We add a redistance to get true distance values.
punctured_brace = Redistance(punctured_brace))
graph = JSONEvaluator()
punctured_brace(graph)
We can now evaluate the graph as usual and get the final shape.
metafold.jobs.run(
"evaluate_graph",
{
"graph": graph.json()
}
)
We now have our final shape that we can render later.