Skip to content

Commit

Permalink
add lightkit class
Browse files Browse the repository at this point in the history
  • Loading branch information
marcomusy committed Jan 12, 2024
1 parent 86e9881 commit 7388fb8
Show file tree
Hide file tree
Showing 5 changed files with 151 additions and 35 deletions.
1 change: 1 addition & 0 deletions docs/changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ point in the same cloud of points.
- fix `image.clone()` in #1011
- add `transformations.TransformInterpolator` class
- add `Line.find_index_at_position()` finds the index of the line vertex that is closest to a point
- add `visual.LightKit` class which provides "natural" lighting from 4 sources.


## Breaking changes
Expand Down
25 changes: 12 additions & 13 deletions vedo/plotter.py
Original file line number Diff line number Diff line change
Expand Up @@ -935,11 +935,14 @@ def add(self, *objs, at=None):
ren.AddActor(a)
except TypeError:
ren.AddActor(a.actor)

if hasattr(a, "rendered_at"):
ir = self.renderers.index(ren)
a.rendered_at.add(ir)
if isinstance(a, vtk.vtkFollower):
a.SetCamera(self.camera)
if isinstance(a, vedo.visual.LightKit):
a.lightkit.AddLightsToRenderer(ren)

return self

Expand Down Expand Up @@ -1038,6 +1041,9 @@ def remove(self, *objs, at=None):
for sha in ob.trail.shadows:
ren.RemoveActor(sha.actor)

elif isinstance(ob, vedo.visual.LightKit):
ob.lightkit.RemoveLightsFromRenderer(ren)

# for i in ids: # WRONG way of doing it!
# del self.objects[i]
# instead we do:
Expand Down Expand Up @@ -2973,25 +2979,18 @@ def _scan_input_return_acts(self, objs):
scanned_acts.append(out)
# scanned_acts.append(vedo.shapes.Text2D(a)) # naive version

# elif isinstance(a, (
# vtk.vtkAssembly,
# vtk.vtkVolume,
# vtk.vtkImageActor,
# vtk.vtkLegendBoxActor,
# vtk.vtkBillboardTextActor3D,
# ),
# ):
# scanned_acts.append(a)

elif isinstance(a, vtk.vtkLight):
self.renderer.AddLight(a)

elif isinstance(a, vtk.vtkPolyData):
scanned_acts.append(vedo.Mesh(a).actor)

elif isinstance(a, vtk.vtkImageData):
scanned_acts.append(vedo.Volume(a).actor)

elif isinstance(a, vtk.vtkLight):
self.renderer.AddLight(a)

elif isinstance(a, vedo.visual.LightKit):
a.lightkit.AddLightsToRenderer(self.renderer)

elif isinstance(a, vtk.get_class("MultiBlockDataSet")):
for i in range(a.GetNumberOfBlocks()):
b = a.GetBlock(i)
Expand Down
51 changes: 29 additions & 22 deletions vedo/shapes.py
Original file line number Diff line number Diff line change
Expand Up @@ -1471,25 +1471,35 @@ def __init__(self, points, r=1.0, cap=True, res=12, c=None, alpha=1.0):
![](https://vedo.embl.es/images/basic/tube.png)
"""
if isinstance(points, vedo.Points):
points = points.vertices

base = np.asarray(points[0], dtype=float)
top = np.asarray(points[-1], dtype=float)

vpoints = vtk.vtkPoints()
idx = len(points)
for p in points:
vpoints.InsertNextPoint(p)
line = vtk.new("PolyLine")
line.GetPointIds().SetNumberOfIds(idx)
for i in range(idx):
line.GetPointIds().SetId(i, i)
lines = vtk.vtkCellArray()
lines.InsertNextCell(line)
polyln = vtk.vtkPolyData()
polyln.SetPoints(vpoints)
polyln.SetLines(lines)
if utils.is_sequence(points):
vpoints = vtk.vtkPoints()
idx = len(points)
for p in points:
vpoints.InsertNextPoint(p)
line = vtk.new("PolyLine")
line.GetPointIds().SetNumberOfIds(idx)
for i in range(idx):
line.GetPointIds().SetId(i, i)
lines = vtk.vtkCellArray()
lines.InsertNextCell(line)
polyln = vtk.vtkPolyData()
polyln.SetPoints(vpoints)
polyln.SetLines(lines)
self.base = np.asarray(points[0], dtype=float)
self.top = np.asarray(points[-1], dtype=float)

elif isinstance(points, Mesh):
polyln = points.dataset
n = polyln.GetNumberOfPoints()
self.base = np.array(polyln.GetPoint(0))
self.top = np.array(polyln.GetPoint(n - 1))

# from vtkmodules.vtkFiltersCore import vtkTubeBender
# bender = vtkTubeBender()
# bender.SetInputData(polyln)
# bender.SetRadius(r)
# bender.Update()
# polyln = bender.GetOutput()

tuf = vtk.new("TubeFilter")
tuf.SetCapping(cap)
Expand Down Expand Up @@ -1525,9 +1535,6 @@ def __init__(self, points, r=1.0, cap=True, res=12, c=None, alpha=1.0):
self.mapper.ScalarVisibilityOn()
self.mapper.SelectColorArray("TubeColors")
self.mapper.Modified()

self.base = base
self.top = top
self.name = "Tube"


Expand Down
108 changes: 108 additions & 0 deletions vedo/visual.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"MeshVisual",
"ImageVisual",
"Actor2D",
"LightKit",
]


Expand Down Expand Up @@ -2849,4 +2850,111 @@ def window(self, value=None):
return self


class LightKit:
"""
A LightKit consists of three lights, a 'key light', a 'fill light', and a 'head light'.
The main light is the key light. It is usually positioned so that it appears like
an overhead light (like the sun, or a ceiling light).
It is generally positioned to shine down on the scene from about a 45 degree angle vertically
and at least a little offset side to side. The key light usually at least about twice as bright
as the total of all other lights in the scene to provide good modeling of object features.
The other lights in the kit (the fill light, headlight, and a pair of back lights)
are weaker sources that provide extra illumination to fill in the spots that the key light misses.
The fill light is usually positioned across from or opposite from the key light
(though still on the same side of the object as the camera) in order to simulate diffuse reflections
from other objects in the scene.
The headlight, always located at the position of the camera, reduces the contrast between areas lit
by the key and fill light. The two back lights, one on the left of the object as seen from the observer
and one on the right, fill on the high-contrast areas behind the object.
To enforce the relationship between the different lights, the intensity of the fill, back and headlights
are set as a ratio to the key light brightness.
Thus, the brightness of all the lights in the scene can be changed by changing the key light intensity.
All lights are directional lights, infinitely far away with no falloff. Lights move with the camera.
For simplicity, the position of lights in the LightKit can only be specified using angles:
the elevation (latitude) and azimuth (longitude) of each light with respect to the camera, in degrees.
For example, a light at (elevation=0, azimuth=0) is located at the camera (a headlight).
A light at (elevation=90, azimuth=0) is above the lookat point, shining down.
Negative azimuth values move the lights clockwise as seen above, positive values counter-clockwise.
So, a light at (elevation=45, azimuth=-20) is above and in front of the object and shining
slightly from the left side.
LightKit limits the colors that can be assigned to any light to those of incandescent sources such as
light bulbs and sunlight. It defines a special color spectrum called "warmth" from which light colors
can be chosen, where 0 is cold blue, 0.5 is neutral white, and 1 is deep sunset red.
Colors close to 0.5 are "cool whites" and "warm whites," respectively.
Since colors far from white on the warmth scale appear less bright, key-to-fill and key-to-headlight
ratios are skewed by key, fill, and headlight colors. If `maintain_luminance` is set, LightKit will
attempt to compensate for these perceptual differences by increasing the brightness of more saturated colors.
To specify the color of a light, positioning etc you can pass a dictionary with the following keys:
- `intensity` : (float) The intensity of the key light. Default is 1.
- `ratio` : (float) The ratio of the light intensity wrt key light.
- `warmth` : (float) The warmth of the light. Default is 0.5.
- `elevation` : (float) The elevation of the light in degrees.
- `azimuth` : (float) The azimuth of the light in degrees.
Example:
```python
from vedo import *
lightkit = LightKit(head={"warmth":0.6))
mesh = Mesh(dataurl+"bunny.obj")
plt = Plotter()
plt.remove_lights().add(mesh, lightkit)
plt.show().close()
```
"""
def __init__(self, key=(), fill=(), back=(), head=(), maintain_luminance=False):

self.lightkit = vtk.new("LightKit")
self.lightkit.SetMaintainLuminance(maintain_luminance)
self.key = dict(key)
self.head = dict(head)
self.fill = dict(fill)
self.back = dict(back)
self.update()

def update(self):
"""Update the LightKit properties."""
if "warmth" in self.key:
self.lightkit.SetKeyLightWarmth(self.key["warmth"])
if "warmth" in self.fill:
self.lightkit.SetFillLightWarmth(self.fill["warmth"])
if "warmth" in self.head:
self.lightkit.SetHeadLightWarmth(self.head["warmth"])
if "warmth" in self.back:
self.lightkit.SetBackLightWarmth(self.back["warmth"])

if "intensity" in self.key:
self.lightkit.SetKeyLightIntensity(self.key["intensity"])
if "ratio" in self.fill:
self.lightkit.SetKeyToFillRatio(self.key["ratio"])
if "ratio" in self.head:
self.lightkit.SetKeyToHeadRatio(self.key["ratio"])
if "ratio" in self.back:
self.lightkit.SetKeyToBackRatio(self.key["ratio"])

if "elevation" in self.key:
self.lightkit.SetKeyLightElevation(self.key["elevation"])
if "elevation" in self.fill:
self.lightkit.SetFillLightElevation(self.fill["elevation"])
if "elevation" in self.head:
self.lightkit.SetHeadLightElevation(self.head["elevation"])
if "elevation" in self.back:
self.lightkit.SetBackLightElevation(self.back["elevation"])

if "azimuth" in self.key:
self.lightkit.SetKeyLightAzimuth(self.key["azimuth"])
if "azimuth" in self.fill:
self.lightkit.SetFillLightAzimuth(self.fill["azimuth"])
if "azimuth" in self.head:
self.lightkit.SetHeadLightAzimuth(self.head["azimuth"])
if "azimuth" in self.back:
self.lightkit.SetBackLightAzimuth(self.back["azimuth"])


1 change: 1 addition & 0 deletions vedo/vtkclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -702,6 +702,7 @@
"vtkInteractorEventRecorder",
"vtkInteractorObserver",
"vtkLight",
"vtkLightKit",
"vtkLogLookupTable",
"vtkMapper",
"vtkPointGaussianMapper",
Expand Down

0 comments on commit 7388fb8

Please sign in to comment.