Clarisse 3.6 SP10 SDK  3.6.0.0.10
 All Classes Namespaces Functions Variables Typedefs Enumerations Enumerator Friends Groups Pages
Manipulating point clouds in script

Table of Contents

This topic overviews how to generate and manipulate point clouds in Python.

What are point clouds?

Point clouds are a key concept in Clarisse. They define 3D points which can be animated, have velocity (for motion blur for example) and have multiple arbitrary properties. They are used as input by deformers to perform deformations or to do geometry scattering and hair and fur interpolation. They can also be used during material shading.

Geometry and point clouds

In Clarisse, any geometry can define point clouds. Point clouds can be defined by point generators such as point array but also by polygon meshes for example. In that case, the point cloud is defined by the poly mesh's vertices.

While any geometry can define point clouds, they don't necessarily do. For example, implicit box (OfClass|GeometryBox) and implicit spheres (OfClass|GeometrySphere) don't define any point clouds. As a matter of fact any geometry which defines point clouds can be deformed.

The Geometry Particle Container

Items of type OfClass|GeometryParticleContainer are static point cloud geometry. Unlike the point array the position of the points of an item of type OfClass|GeometryParticleContainer is not generated by an associated module. They are baked statically into the geometry.

Particle containers or static point clouds can't be manipulated directly by the user. They can only be modified through the SDK or dedicated tools such as the Particle Paint or the Property Paint.

The following example forms a cube filled with points equally spaced on all 3 axis.

point_cloud_cube.png


1 x_count = 10
2 y_count = 10
3 z_count = 10
4 count = x_count * y_count * z_count
5 vertices = ix.api.GMathVec3fArray(count)
6 
7 # setting vertices
8 for i in range(count):
9  x = float(i % x_count) / (x_count - 1)
10  y = float((i / x_count) % y_count) / (y_count - 1)
11  z = float(i / (x_count * y_count) % z_count) / (z_count - 1)
12  vertices[i] = ix.api.GMathVec3f(x - 0.5, y - 0.5, z - 0.5)
13 
14 #creating the item of type OfClass|GeometryParticleContainer
15 ix.api.IOHelpers.create_particles(ix.application, "cube_ptc", vertices)

The script fills a vertex array by spacing points equally on all axis. To create a new item of type OfClass|GeometryParticleContainer with our vertices we just need to call:

1 ix.api.IOHelpers.create_particles(ix.application, "cube_ptc", vertices)
Note
Unlike a point generator such as the point array or the point cloud, the geometry data information is cached and stored in the project file. The size of the point cloud data can get pretty big. The best practice is then to save each static point clouds geometry in a single project file then reference them when needed. This way the size of the main project file remains relatively small.

Baking point clouds from a geometry

Sometimes it can be useful to bake point clouds specially when the point cloud generation relies on a heavy and slow process. Instead of having a live setup, which updates more or less slowly each time the dependency graph is modified, you can decide to bake the resulting point cloud in a static point cloud geometry.

Baking positions

The following basic script bakes the point cloud position into a new point cloud geometry. To do so it accesses to the point cloud of a selected geometry. It then extracts the point positions and create a new point cloud item using these positions.

point_cloud.png


1 # This script copies the point cloud of a geometry
2 # into a new point cloud geometry
3 if ix.selection.get_count() > 0:
4  item = ix.selection[0]
5  if item.get_module().is_geometry():
6  # only items of class Geometry define point clouds
7  geo = item.get_module().get_geometry()
8  ptc = geo.get_point_cloud()
9  # some geometries such as implicit sphere (OfClass|GeometrySphere) may not define point clouds
10  if ptc and ptc.has_positions():
11  positions = ix.api.GMathVec3fArray()
12  ptc.get_positions(positions)
13  parts = ix.api.IOHelpers.create_particles(ix.application, item.get_name() + "_ptc", positions)

ModuleGeometry::get_geometry returns the underlying GeometryObject. Note that this method has an optional argument that let you retrieve either the deformed or non-deformed version of the geometry. Depending of the geometry you've requested you'll get non deformed point clouds (the base geometry) or deformed one.

1 ptc = geo.get_point_cloud()
2 ...
3 positions = ix.api.GMathVec3fArray()
4 ptc.get_positions(positions)

This fills the input array with the point positions. To create a new geometry point cloud with the positions we then simply need to call:

1 parts = ix.api.IOHelpers.create_particles(ix.application, item.get_name() + "_ptc", positions)

This IOHelpers::create_particles creates a new static point cloud geometry for which data is serialized in the project file. Please note this method has several optional arguments to set point normals and point velocities for example.

Working with point clouds properties

Arbitrary data such as velocity, normals, quaternions etc.. can be attached to each point of a point cloud. This data is often called Attribute in other packages. In Clarisse, it is called Property to avoid any confusion with item attributes. Properties are always identified by a global name and are organized in a collection GeometryPropertyCollection. This data can be accessed during shading by using an Extract Property texture node.

Creating point clouds properties

We've seen how to create an item of type OfClass|GeometryParticleContainer with custom positions. Now we are going to see how we can attach custom properties to these points. To do so we've modified the script that was filling a cube with point equally spaced on all axes.

1 # This script creates a point cloud with 2 properties
2 
3 x_count = 10
4 y_count = 10
5 z_count = 10
6 count = x_count * y_count * z_count
7 vertices = ix.api.GMathVec3fArray(count)
8 
9 # creating properties
10 colors = ix.api.ResourceProperty("colors")
11 ranks = ix.api.ResourceProperty("ranks")
12 colors.init(ix.api.ResourceProperty.TYPE_FLOAT_32, 3, count)
13 ranks.init(ix.api.ResourceProperty.TYPE_FLOAT_32, 1, count)
14 
15 # setting vertices
16 for i in range(count):
17  x = float(i % x_count) / (x_count - 1)
18  y = float((i / x_count) % y_count) / (y_count - 1)
19  z = float(i / (x_count * y_count) % z_count) / (z_count - 1)
20 
21  vertices[i] = ix.api.GMathVec3f(x - 0.5, y - 0.5, z - 0.5)
22 
23  # filling properties
24  colors.set_float(i, x, 0)
25  colors.set_float(i, y, 1)
26  colors.set_float(i, z, 2)
27  ranks.set_float(i, float(i) / float(count - 1))
28 
29 # creating an array of properties
30 properties = ix.api.ResourcePropertyArray(2)
31 properties[0] = colors
32 properties[1] = ranks
33 
34 item = ix.api.IOHelpers.create_particles(ix.application, "cube_ptc", vertices)
35 ix.api.IOHelpers.set_particles_properties(item, properties)

If you run the script it will create the same cube as in the previous script. However, 2 new properties named colors and ranks are now defined by the geometry this time. To achieve this we slightly modified the original script.

1 colors = ix.api.ResourceProperty("colors")
2 ranks = ix.api.ResourceProperty("ranks")

Here we are creating two new property called color and ranks

1 colors.init(ix.api.ResourceProperty.TYPE_FLOAT_32, 3, count)
2 ranks.init(ix.api.ResourceProperty.TYPE_FLOAT_32, 1, count)

As you can see properties are not directly attached to points. They are defined in arrays that should match the number of points of the point cloud why is why we set the size of each property to count.

The depth of the property must also be set during the initialization call. Here we we store 3 values as float (r,g,b) per point for colors and a single float value for ranks.

1 colors.set_float(i, x, 0)
2 colors.set_float(i, y, 1)
3 colors.set_float(i, z, 2)
4 ranks.set_float(i, float(i) / float(count - 1))

We just set the property values for each point.

1 properties = ix.api.ResourcePropertyArray(2)
2 properties[0] = colors
3 properties[1] = ranks

Before setting our properties to our geometry, we need to arrange them in a ResourcePropertyArray.

1 ix.api.IOHelpers.set_particles_properties(item, properties)

Finally we inject our properties into our newly created item of type OfClass|GeometryParticleContainer. As the name of the method suggests IOHelpers::set_particles_properties, this method can be use to set new properties to an existing item of type OfClass|GeometryParticle. In other words, you can modify properties in place without the need of destroying and re-creating a new geometry.

Note
OfClass|GeometryParticleContainer is the only class of geometry which supports adding, modifying or removing properties.

Accessing to existing point clouds properties

Point clouds properties are organized in collections that can be found in ModuleGeometry. To get a handle to the property collection of a geometry you simply need to use ModuleGeometry::get_properties.

Note
ModuleGeometry::get_properties can return NULL or None if no property is available. Make sure to always check what is returned by this method.

Baking points and properties

In the final example, we are going to see how we can bake the point cloud of an arbitrary geometry along with its normals, velocities and arbitrary properties.

1 if ix.selection.get_count() > 0:
2  item = ix.selection[0]
3  if item.get_module().is_geometry():
4  # only items of class Geometry define point clouds
5  geo = item.get_module().get_geometry()
6  ptc = geo.get_point_cloud()
7  # some geometries such as implicit sphere (OfClass|GeometrySphere) may not define point clouds
8  if ptc:
9  positions = ix.api.GMathVec3fArray()
10  normals = ix.api.GMathVec3fArray()
11  velocities = ix.api.GMathVec3fArray()
12  if ptc.has_positions(): ptc.get_positions(positions)
13  if ptc.has_normals(): ptc.get_normals(normals)
14  if ptc.has_velocities(): ptc.get_velocities(velocities)
15  parts = ix.api.IOHelpers.create_particles(ix.application, item.get_name() + "_ptc", positions, normals, velocities)
16  prop_collection = obj.get_module().get_properties()
17  if prop_collection:
18  properties = ix.api.GeometryPointPropertyVector()
19  for i in range(prop_collection.get_property_count()):
20  prop = prop_collection.get_property(i)
21  if prop.is_kindof(ix.api.GeometryPointProperty.class_info()):
22  properties.add(prop)
23  ix.api.IOHelpers.set_particles_properties(parts, properties)

As mentionned previously geometries can optionally define a point cloud which can optionally define positions, normals or velocities. This is the reason why we are using the following:

1 if ptc.has_positions(): ptc.get_positions(positions)
2 if ptc.has_normals(): ptc.get_normals(normals)
3 if ptc.has_velocities(): ptc.get_velocities(velocities)

Next we need to iterate on properties of the current geometry. Unlike positions, normals or velocities for which you need to get the GeometryPointCloud, custom properties are accessible through the module using ModuleGeometry::get_properties. Again, the property collection can be NULL if no property is available on the selected item.

1 for i in range(prop_collection.get_property_count()):
2  prop = prop_collection.get_property(i)
3  if prop.is_kindof(ix.api.GeometryPointProperty.class_info()):
4  properties.add(prop)

As you can notice, we are checking if the property is of class GeometryPointProperty. In Clarisse there are different kinds of properties each for different purpose. Items of OfClass|GeometryParticleContainer only support properties of class GeometryPointProperty which is why we filter incoming property by GeometryPointProperty class info.

1 ix.api.IOHelpers.set_particles_properties(parts, properties)

Finally we add the properties to the newly created item.