VTK: achieve lighting effect, from a line to a face

Posted by dysonline on Thu, 17 Feb 2022 01:41:42 +0100

Guide: many places need to check the illumination, and there are many software to deal with it. Today I made a content about lighting effect. From 10 a.m. to 3:30 p.m., start writing headlines. About that long. There was no rest or meal at noon.

Content:

1. Start with the simplest case

2. My Lighting class

3. Basic parameter setting

4. Grid import and basic initialization (update_grid function)

Update light function (u#5)

6. Set a field to display the effect of light (update_shadow function)

7. Shiny appearance and visualization of results

8. End (with all codes attached)

A simple operation, if there is no change, repeated 10000 times, is still just an operation.

A simple operation, if there are two variables, repeated 10000 times, is a system.

1. Start with a simple case

There is a case in pyvita. Find an example of starting from a point and intersecting with the grid.

As shown in the figure, the blue one is a line, and the intersection with the sphere is displayed by points.

In this simple case, we can analyze what we need to know in order to obtain the intersection on the surface.

(1) a starting point is required

  start

(2) need an end point,

  end,

(3) a straight line is required,

(4) one face needs to be defined

(5) find the intersection

Let's take a look at the code of this simple case:

  import pyvista as pv
  import numpy as np
  # Create source to ray trace
  sphere=pv.Sphere(radius=0.85)
  # Define line segment
  start=[0, 0, 0]
  stop=[0.25, 1, 0.5]
  # Perform ray trace
  points, ind=sphere.ray_trace(start, stop)
  # # Create geometry to represent ray trace
  ray=pv.Line(start, stop)
  intersection=pv.PolyData(points)
  #
  # # Render the result
  p=pv.Plotter()
  p.add_mesh(sphere,
  show_edges=True, opacity=0.5, color="w",
  lighting=False, label="Test Mesh")
  p.add_mesh(ray, color="blue", line_width=5, label="Ray Segment")
  p.add_mesh(intersection, color="maroon",
  point_size=25, label="Intersection Points")
  p.add_legend()
  p()

In fact, the four lines 5, 8, 9 and 12 complete the main operations.

Line 5: defines a spherical mesh

Line 8-9: defines the start and end points

  Line 12,: ray_ The trace function gets the intersection.

There are some realistic contents behind.

2. My Lighting class

I constructed a class and operated it completely according to the simple case above. Form the effect of the light in the figure below.

The blue circle behind is the display of the end point.

Let's first look at the basic structure:

In addition to some basic parameters, there are only three steps, which correspond to the above three functions:

  update_grid

  update_lights

  update_shadow

3. Basic parameter setting

In__ init__ In the function, we define nine parameters.

Input parameters

Source represents the coordinates of the point light source

Angle represents the angle at which the point light diffuses to form a cone

Direction represents the direction of the light center and is a unit vector

seeds is a number of parameters needed to obtain the coordinates of the light end point

In addition to these four, the instance variables of the class are increased

Grid is used to store grid data

distance sets the length of the light cone

nlights records the number of rays

  seed_polydata is visual data that stores light spots

In order to calculate the distance between two points, I defined a mag function

  @staticmethod
  def mag(start,end):
  s=(start[0] - end[0]) ** 2 + (start[1] - end[1]) ** 2 + (start[2] - end[2]) ** 2
  return s ** 0.5

There should be similar functions in the library, but I'm too lazy to check.

There are two more attributes

  @property
  def source_polydata(self):
  return pv.PolyData(self.source)
  @property
  def aim_radius(self):
  return np.tan(self.angle) * self.distance

The first one is to generate visual data of point light source

Second, aim_radius is the radius of the end point of the target.

After starting from the point light source, we hash the points on the bottom to determine the number of rays.

4. Grid import and basic initialization (update_grid function)

First, we have to deal with the grid. And initialize some Game download Data, such as distance.

Let's first look at the function code:

  def update_grid(self,filename):
  self.grid=pv.read(filename)
  xmin,xmax,ymin,ymax,zmin,zmax=self.grid.bounds
  start=[xmin,ymin,zmin]
  end=[xmax,ymax,zmax]
  dis=self.mag(start,end)
  p=[[xmin, ymin, zmin],
  [xmin, ymin, zmax],
  [xmin, ymax, zmin],
  [xmin, ymax, zmax],
  [xmax, ymin, zmin],
  [xmax, ymin, zmax],
  [xmax, ymax, zmin],
  [xmax, ymax, zmax]
  ]
  for i in p:
  self.distance=max(dis, self.mag(self.source, i))
  self.distance=self.distance*1.05

(1) read grid from file

(2) calculate the distance to determine that the light emitted by the point light source intersects all positions of the grid

5. Set the light (update_lights function)

That is to say, the data we need to extract. Take a look at the code:

  def update_lights(self):
  # Generate face
  # Terminal Center
  end_center=[self.source[0] + self.distance * self.direction[0],
  self.source[1] + self.distance * self.direction[1],
  self.source[2] + self.distance * self.direction[2]
  ]
  plane=pv.Plane(center=end_center, direction=self.direction,
  i_size=2 * self.aim_radius, j_size=2 * self.aim_radius,
  i_resolution=self.seeds, j_resolution=self.seeds)
  # Define end point
  stops=[]
  for point in plane.points:
  x1=point[0]
  y1=point[1]
  z1=point[2]
  if self.mag(point, end_center) < self.aim_radius:
  stops.append([x1, y1, z1])
  self.nlights=len(stops)
  self.lights=stops
  self.seed_polydata=pv.PolyData(stops)

(1) generate a plane data centered on the bottom center.

(2) extract the data of points belonging to the ball

(3) generate a polydata for visualization

6. Set a field to display the effect of light (update_shadow function)

The name may not be very good. The word shadow is used in the place where the light shines.

Also, look at the code first:

  def update_shadow(self):
  shadow_cell_ids=[]
  for i in range(self.nlights):
  point, ind=self.grid.ray_trace(self.source, self.lights[i])
  if len(point) < 1:
  pass
  else:
  a=self.mag(self.source, point[0])
  end=ind[0]
  for i in point:
  if self.mag(self.source, i) < a:
  a=self.mag(self.source, i)
  end=ind
  else:
  end=ind[0]
  shadow_cell_ids.append(end)
  # Create shadow cell property
  shadow=np.zeros((self.grid.n_cells,))
  for i in shadow_cell_ids:
  shadow[i]=10
  self.grid.cell_arrays['shadow']=shadow

(1) generate an id list, which is generated by ray_trace generates a list of face numbers.

At the same time, it should be noted that ray_trace will intersect more than two points with the surface. We choose the nearest one because the light will not pass through the object.

(2) generate a scalar attribute of shadow. The face in the list is assigned 10 and the others are 0

7. Shiny appearance and visualization of results

Finally, we actually wrote an update function.

  def update(self,filename):
  self.update_grid(filename)
  self.update_lights()
  self.update_shadow()

Mainly for the convenience of writing.

Let's take a look at the way we call:

  if __name__=='__main__':
  source=[3000,0,-2000]
  angle=20
  direction=[-0.3,0.1,1]
  seeds=100
  light=Lighting(source,angle,direction,seeds)
  light.update('car.stl')
  p=pv.Plotter()
  p.add_background_image('road.jpg')
  #
  p.add_mesh(light.source_polydata, color="yellow", point_size=20, render_points_as_spheres=True)
  p.add_mesh(light.seed_polydata, color="blue", line_width=5, label="Ray Segment")
  p.add_mesh(light.grid, scalars='shadow')
  # p.add_legend()
  p()

Set four parameters, call the update function at the same time, and read the car STL file. And show it.

The result is the effect as we said above.

8. Conclusion

A small function, put in practical application, will have a lot of incredible things. In fact, many times, we don't use so much knowledge. More likely is the form of organization.

I'm Dr. Zhang Lin. please remember to pay attention. If you are interested in anything, you can also reply in the message area.

Good luck!

Finally, attach all codes:

  import pyvista as pv
  import numpy as np
  class Lighting:
  def __init__(self, source, angle, direction, seeds=10):
  self.direction=direction
  self.source=source
  self.angle=angle/180*np.pi
  self.seeds=seeds
  # Determine the distance based on the entered grid
  self.grid=None
  self.distance=None
  # Generate light seed
  self.nlights=None
  self.lights=None
  self.seed_polydata=None
  @staticmethod
  def mag(start,end):
  s=(start[0] - end[0]) ** 2 + (start[1] - end[1]) ** 2 + (start[2] - end[2]) ** 2
  return s ** 0.5
  @property
  def source_polydata(self):
  return pv.PolyData(self.source)
  @property
  def aim_radius(self):
  return np.tan(self.angle) * self.distance
  def update_grid(self,filename):
  self.grid=pv.read(filename)
  xmin,xmax,ymin,ymax,zmin,zmax=self.grid.bounds
  start=[xmin,ymin,zmin]
  end=[xmax,ymax,zmax]
  dis=self.mag(start,end)
  p=[[xmin, ymin, zmin],
  [xmin, ymin, zmax],
  [xmin, ymax, zmin],
  [xmin, ymax, zmax],
  [xmax, ymin, zmin],
  [xmax, ymin, zmax],
  [xmax, ymax, zmin],
  [xmax, ymax, zmax]
  ]
  for i in p:
  self.distance=max(dis, self.mag(self.source, i))
  self.distance=self.distance*1.05
  def update_lights(self):
  # Generate face
  # Terminal Center
  end_center=[self.source[0] + self.distance * self.direction[0],
  self.source[1] + self.distance * self.direction[1],
  self.source[2] + self.distance * self.direction[2]
  ]
  plane=pv.Plane(center=end_center, direction=self.direction,
  i_size=2 * self.aim_radius, j_size=2 * self.aim_radius,
  i_resolution=self.seeds, j_resolution=self.seeds)
  # Define end point
  stops=[]
  for point in plane.points:
  x1=point[0]
  y1=point[1]
  z1=point[2]
  if self.mag(point, end_center) < self.aim_radius:
  stops.append([x1, y1, z1])
  self.nlights=len(stops)
  self.lights=stops
  self.seed_polydata=pv.PolyData(stops)
  def update_shadow(self):
  shadow_cell_ids=[]
  for i in range(self.nlights):
  point, ind=self.grid.ray_trace(self.source, self.lights[i])
  if len(point) < 1:
  pass
  else:
  a=self.mag(self.source, point[0])
  end=ind[0]
  for i in point:
  if self.mag(self.source, i) < a:
  a=self.mag(self.source, i)
  end=ind
  else:
  end=ind[0]
  shadow_cell_ids.append(end)
  # Create shadow cell property
  shadow=np.zeros((self.grid.n_cells,))
  for i in shadow_cell_ids:
  shadow[i]=10
  self.grid.cell_arrays['shadow']=shadow
  def update(self,filename):
  self.update_grid(filename)
  self.update_lights()
  self.update_shadow()
  if __name__=='__main__':
  source=[3000,0,-2000]
  angle=20
  direction=[-0.3,0.1,1]
  seeds=100
  light=Lighting(source,angle,direction,seeds)
  light.update('car.stl')
  p=pv.Plotter()
  p.add_background_image('road.jpg')
  #
  p.add_mesh(light.source_polydata, color="yellow", point_size=20, render_points_as_spheres=True)
  p.add_mesh(light.seed_polydata, color="blue", line_width=5, label="Ray Segment")
  p.add_mesh(light.grid, scalars='shadow')
  # p.add_legend()
  p()