7
$\begingroup$

Background

I want to render 3D points with color information for scientific visualization. I have several hundred thousands of points up to 2 million. For each point I have [xyz] coordinates and two scalars [t, tau]. The scalars should be shown as color and are currently mapped to uv-coordinates.

Wanted result

I want to achieve something like this but with (lots) more points/spheres:

---

Current solution

My workflow

  1. load the data
  2. create/copy a sphere mesh
  3. move the copied mesh to [xyz] coordinates
  4. define the uv coordinates for the whole mesh with respect to [t, tau]
  5. repeat 2. to 4. for each data entry
  6. join all created meshes

Problem

While loading the points and creating the scene I get a slowdown. The first 1000 points work fine, but the overall process gets quickly slower. I am sure it has something to do with the commands I use from the API and the amount of objects I create.

I had two ideas to solve the problem - neither worked:

  1. Create a sphere mesh for each data entry and join them in the end. This leads to a huge amount of created objects, which slows down blender (compare this question).
  2. Create the meshes for a subset of the data entries and join these to a single object/mesh. Do this until all points are loaded. This leads to the problem, that the joining process gets more complicated and increasingly takes more time.

Questions

  1. Are there better commands I can use to speed up the process?
  2. Is there a better solution than copy a sphere mesh for each data entry and adjust its UV-coordinates for coloring. E.g. create one mesh and add vertices to it - how can I alter the UV-coordinates this way.
  3. Is there generally a better way to render a colored point cloud, e.g. this one is missing the colors.
$\endgroup$
  • 2
    $\begingroup$ Consider using a different filehosting for your py and data files. Not a big fan of having to sign up to download. Might want to check out Point Cloud Visualizer $\endgroup$ – batFINGER Mar 21 '18 at 12:55
  • $\begingroup$ @batFinger Thank you for the hint! I moved files to github. I will also check Point Cloud Visualizer $\endgroup$ – Thomas Seidl Mar 21 '18 at 13:05
  • 2
    $\begingroup$ In order to give you specific answers please only ask one question at a time. $\endgroup$ – Ray Mairlot Mar 21 '18 at 13:59
  • 2
    $\begingroup$ Well the way I see it this is going to be a problem regardless of the used technique, 2 million spheres is a lot. You are probably just hitting inherent performance limitations of Blender, the viewport code and general Python speed. From a rendering performance point of view, it would theoretically be better to have only one sphere and instance it around (a lot less geometry data to store at least) and not merge anything at all. That being said I don't think the viewport performance and general Blender operations (like selection and undo) are gonna like it a lot regardless $\endgroup$ – Duarte Farrajota Ramos Mar 21 '18 at 18:15
  • $\begingroup$ @Duarte: Thank you for the input. Create instances of the sphere maybe another solution. I have searched around and also got a clue for the coloring problem. I will keep this question alive and update my solution. $\endgroup$ – Thomas Seidl Mar 21 '18 at 21:10
4
$\begingroup$

Updated:

This script will add the pointcloud data as mesh vertices, then create a low-poly icosphere as an instanced object on each vert (using mesh.from_pydata instead of bmesh as advised by BatFINGER).

Currently this does not use the UV data in the latter two columns in the data file. I've also scaled the coordiantes by a factor of 25 (can be changed in the script) to make everything more visible and easier to manipulate in the viewport. This script takes ~0.075 seconds on my laptop for the 10,001 verts in this file.

import bpy
import numpy as np
from mathutils import Vector
from time import time

t = time()
C = bpy.context

scale = 25

# Open textfile to numpy array and scale
pointcloudfilepath = '/Users/username/Downloads/point_data.txt'
with open( pointcloudfilepath ) as fh:
    data = np.loadtxt( fh ) * scale

print( f'vertcount = {len(data)}')

# Create and arrange mesh data
verts = [ Vector( data[i,:3] ) for i in range(data.shape[0]) ]
m     = bpy.data.meshes.new('pc')
m.from_pydata(verts, [], [])

# Create mesh object and link to scene collection
o = bpy.data.objects.new( 'pc', m )
C.scene.collection.objects.link( o )

# Add minimal icosphere
bpy.ops.mesh.primitive_ico_sphere_add( subdivisions = 1, radius = 0.05 )
isobj = bpy.data.objects[ C.object.name ]

# Set instancing props
for ob in [ isobj, o ]:
    ob.instance_type               = 'VERTS'
    ob.show_instancer_for_viewport = False
    ob.show_instancer_for_render   = False

# Set instance parenting (parent icosphere to verts)
o.select_set(True)
C.view_layer.objects.active = o

bpy.ops.object.parent_set( type = 'VERTEX', keep_transform = True )

print( f'Total time = {time() - t} seconds' )

I've used each sphere's location as material color in a simple shader, you can apply any other logic that fits your requirements better. Here's the node setup:

enter image description here

And final result:

enter image description here

$\endgroup$
  • 1
    $\begingroup$ Mesh.from_pydata() is possibly quicker in this case. $\endgroup$ – batFINGER Mar 29 at 2:46
  • 1
    $\begingroup$ Cool, how are you finding 2.8? Wonder if there is a way to use vert color here. $\endgroup$ – batFINGER Mar 31 at 10:11
  • 2
    $\begingroup$ Nice and mostly stable, trying to get used to the changes but no complaints all in all, and the new features are awesome. My first thought was using vert colors, but it requires adding faces which seems to not make sense in this case unless explicit face data is also provided. $\endgroup$ – TLousky Mar 31 at 10:14
2
$\begingroup$

I've been looking at the same problem. There is a solution by some astrophysicists that can bring in massive amount of points or voxels but it does involve a bit of work to convert the point clouds.

Its been a while since I looked at it but essentially you need use a bit of python to convert your point cloud into coordinates within a certain cube and normalize the values. I confess I haven't fully completed the instructions myself after hitting a snag so best to to follow the video!

https://www.youtube.com/watch?v=zmY_mn6Ue2g&t=0s&list=PLdeObAW1huF4suWRiENa43AYcY72v78jA&index=10

$\endgroup$
  • $\begingroup$ Link only answers are frowned on here, can you please add some detail in your actual answer describing the solution? $\endgroup$ – Sazerac Mar 28 at 1:33
  • $\begingroup$ Well its been a while since I looked at it but essentially you need to convert your point cloud into coordinates within a certain cube and normalize the values. I confess I haven't fully completed the instructions myself after hitting a snag so best to to follow the video! $\endgroup$ – John McCarthy Mar 28 at 1:49
  • $\begingroup$ just edit the answer with the best description you can provide, It can just be an summary of the method or whatever. $\endgroup$ – Sazerac Mar 28 at 1:52
1
$\begingroup$

Point Density Node.

For the colours could look at using a point density node. The material is given to the domain object. World coordinates are used, the mesh is contained within the domain.

The vertices of the mesh are used for the points. The normals for colour.

enter image description here 8x8x8 dot colour cube

Here is a script to set up the colour cube as shown in image above. Bmesh allows non normalized normals to be set on the vertices.

import bpy
import bmesh
from mathutils import Vector

context = bpy.context

bpy.ops.mesh.primitive_cube_add()

bm = bmesh.new()
me = context.object.data
bm.from_mesh(me)
bmesh.ops.subdivide_edges(bm,
        edges=bm.edges,
        use_grid_fill=True,
        cuts=6)
for e in bm.edges:
    bm.edges.remove(e)
for v in bm.verts:
    v.normal = (v.co + Vector((1, 1, 1))) / 2
bm.to_mesh(me)
me.update()

Borrowing heavily from @TLousky's answer.

Similarly imports your data as a mesh of vertices. In bmesh each vert is assigned an rgb normal, calculated from tau and t. Read that HLS colour space is akin to polar coordinates, have simply set h, s, v = tau, t, 1 by way of example

This could be quicker without bmesh, using me.vertices.foreach_set("normal", flat_list_of_coords) but in my testing, the normals are normalized when set. Could get around this by calculating a u, v set the normal to (u, v, 1) and re-scale later, by reciprocal of normalized z.

import colorsys
import bmesh
import bpy
import numpy as np
from mathutils import Vector
from time import time

t = time()
context = bpy.context
scene = context.scene
scale = 1

pointcloudfilepath = '/home/batfinger/Desktop/point_data.txt'
with open(pointcloudfilepath) as fh:
    data = np.loadtxt(fh)

print(f'vertcount = {len(data)}')

verts = [data[i, :5] for i in range(data.shape[0])]

me = bpy.data.meshes.new('pc')

bm = bmesh.new()
bm.from_mesh(me)
for x, y, z, t, tau in verts:
    co = scale * Vector((x, y, z))
    v = bm.verts.new(co)
    v.normal = colorsys.hls_to_rgb(tau, t, 1)

bm.to_mesh(me)

ob = bpy.data.objects.new('pc', me)
scene.collection.objects.link(ob)

I have added the domain object and its material manually.

To render point density need to use cycles.

enter image description here Test data showing calculated colour. Adjusting radius and resolution, and armed with a better knowledge of cycles / nodes than me could most likely create a much better result.

$\endgroup$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.