Yiwen-ntu's picture
Upload 47 files
7262fda verified
raw
history blame contribute delete
No virus
23.2 kB
import numpy as np
from ipywidgets import embed
import pythreejs as p3s
import uuid
from .color_util import get_colors, gen_circle, gen_checkers
EMBED_URL = "https://cdn.jsdelivr.net/npm/@jupyter-widgets/html-manager@1.0.1/dist/embed-amd.js"
class PyThreeJSViewer(object):
def __init__(self, settings, render_mode="WEBSITE"):
self.render_mode = render_mode
self.__update_settings(settings)
self._light = p3s.DirectionalLight(color='white', position=[0, 0, 1], intensity=0.6)
self._light2 = p3s.AmbientLight(intensity=0.5)
self._cam = p3s.PerspectiveCamera(position=[0, 0, 1], lookAt=[0, 0, 0], fov=self.__s["fov"],
aspect=self.__s["width"] / self.__s["height"], children=[self._light])
self._orbit = p3s.OrbitControls(controlling=self._cam)
self._scene = p3s.Scene(children=[self._cam, self._light2], background=self.__s["background"]) # "#4c4c80"
self._renderer = p3s.Renderer(camera=self._cam, scene=self._scene, controls=[self._orbit],
width=self.__s["width"], height=self.__s["height"],
antialias=self.__s["antialias"])
self.__objects = {}
self.__cnt = 0
def jupyter_mode(self):
self.render_mode = "JUPYTER"
def offline(self):
self.render_mode = "OFFLINE"
def website(self):
self.render_mode = "WEBSITE"
def __get_shading(self, shading):
shad = {"flat": True, "wireframe": False, "wire_width": 0.03, "wire_color": "black",
"side": 'DoubleSide', "colormap": "viridis", "normalize": [None, None],
"bbox": False, "roughness": 0.5, "metalness": 0.25, "reflectivity": 1.0,
"line_width": 1.0, "line_color": "black",
"point_color": "red", "point_size": 0.01, "point_shape": "circle",
"text_color": "red"
}
for k in shading:
shad[k] = shading[k]
return shad
def __update_settings(self, settings={}):
sett = {"width": 600, "height": 600, "antialias": True, "scale": 1.5, "background": "#ffffff",
"fov": 30}
for k in settings:
sett[k] = settings[k]
self.__s = sett
def __add_object(self, obj, parent=None):
if not parent: # Object is added to global scene and objects dict
self.__objects[self.__cnt] = obj
self.__cnt += 1
self._scene.add(obj["mesh"])
else: # Object is added to parent object and NOT to objects dict
parent.add(obj["mesh"])
self.__update_view()
if self.render_mode == "JUPYTER":
return self.__cnt - 1
elif self.render_mode == "WEBSITE":
return self
def __add_line_geometry(self, lines, shading, obj=None):
lines = lines.astype("float32", copy=False)
mi = np.min(lines, axis=0)
ma = np.max(lines, axis=0)
geometry = p3s.LineSegmentsGeometry(positions=lines.reshape((-1, 2, 3)))
material = p3s.LineMaterial(linewidth=shading["line_width"], color=shading["line_color"])
# , vertexColors='VertexColors'),
lines = p3s.LineSegments2(geometry=geometry, material=material) # type='LinePieces')
line_obj = {"geometry": geometry, "mesh": lines, "material": material,
"max": ma, "min": mi, "type": "Lines", "wireframe": None}
if obj:
return self.__add_object(line_obj, obj), line_obj
else:
return self.__add_object(line_obj)
def __update_view(self):
if len(self.__objects) == 0:
return
ma = np.zeros((len(self.__objects), 3))
mi = np.zeros((len(self.__objects), 3))
for r, obj in enumerate(self.__objects):
ma[r] = self.__objects[obj]["max"]
mi[r] = self.__objects[obj]["min"]
ma = np.max(ma, axis=0)
mi = np.min(mi, axis=0)
diag = np.linalg.norm(ma - mi)
mean = ((ma - mi) / 2 + mi).tolist()
scale = self.__s["scale"] * (diag)
self._orbit.target = mean
self._cam.lookAt(mean)
self._cam.position = [mean[0], mean[1], mean[2] + scale]
self._light.position = [mean[0], mean[1], mean[2] + scale]
self._orbit.exec_three_obj_method('update')
self._cam.exec_three_obj_method('updateProjectionMatrix')
def __get_bbox(self, v):
m = np.min(v, axis=0)
M = np.max(v, axis=0)
# Corners of the bounding box
v_box = np.array([[m[0], m[1], m[2]], [M[0], m[1], m[2]], [M[0], M[1], m[2]], [m[0], M[1], m[2]],
[m[0], m[1], M[2]], [M[0], m[1], M[2]], [M[0], M[1], M[2]], [m[0], M[1], M[2]]])
f_box = np.array([[0, 1], [1, 2], [2, 3], [3, 0], [4, 5], [5, 6], [6, 7], [7, 4],
[0, 4], [1, 5], [2, 6], [7, 3]], dtype=np.uint32)
return v_box, f_box
def __get_colors(self, v, f, c, sh):
coloring = "VertexColors"
if type(c) == np.ndarray and c.size == 3: # Single color
colors = np.ones_like(v)
colors[:, 0] = c[0]
colors[:, 1] = c[1]
colors[:, 2] = c[2]
# print("Single colors")
elif type(c) == np.ndarray and len(c.shape) == 2 and c.shape[1] == 3: # Color values for
if c.shape[0] == f.shape[0]: # faces
colors = np.hstack([c, c, c]).reshape((-1, 3))
coloring = "FaceColors"
# print("Face color values")
elif c.shape[0] == v.shape[0]: # vertices
colors = c
# print("Vertex color values")
else: # Wrong size, fallback
print("Invalid color array given! Supported are numpy arrays.", type(c))
colors = np.ones_like(v)
colors[:, 0] = 1.0
colors[:, 1] = 0.874
colors[:, 2] = 0.0
elif type(c) == np.ndarray and c.size == f.shape[0]: # Function values for faces
normalize = sh["normalize"][0] != None and sh["normalize"][1] != None
cc = get_colors(c, sh["colormap"], normalize=normalize,
vmin=sh["normalize"][0], vmax=sh["normalize"][1])
# print(cc.shape)
colors = np.hstack([cc, cc, cc]).reshape((-1, 3))
coloring = "FaceColors"
# print("Face function values")
elif type(c) == np.ndarray and c.size == v.shape[0]: # Function values for vertices
normalize = sh["normalize"][0] != None and sh["normalize"][1] != None
colors = get_colors(c, sh["colormap"], normalize=normalize,
vmin=sh["normalize"][0], vmax=sh["normalize"][1])
# print("Vertex function values")
else:
colors = np.ones_like(v)
colors[:, 0] = 1.0
colors[:, 1] = 0.874
colors[:, 2] = 0.0
# No color
if c is not None:
print("Invalid color array given! Supported are numpy arrays.", type(c))
return colors, coloring
def __get_point_colors(self, v, c, sh):
v_color = True
if c is None: # No color given, use global color
# conv = mpl.colors.ColorConverter()
colors = sh["point_color"] # np.array(conv.to_rgb(sh["point_color"]))
v_color = False
elif isinstance(c, str): # No color given, use global color
# conv = mpl.colors.ColorConverter()
colors = c # np.array(conv.to_rgb(c))
v_color = False
elif type(c) == np.ndarray and len(c.shape) == 2 and c.shape[0] == v.shape[0] and c.shape[1] == 3:
# Point color
colors = c.astype("float32", copy=False)
elif isinstance(c, np.ndarray) and len(c.shape) == 2 and c.shape[0] == v.shape[0] and c.shape[1] != 3:
# Function values for vertices, but the colors are features
c_norm = np.linalg.norm(c, ord=2, axis=-1)
normalize = sh["normalize"][0] != None and sh["normalize"][1] != None
colors = get_colors(c_norm, sh["colormap"], normalize=normalize,
vmin=sh["normalize"][0], vmax=sh["normalize"][1])
colors = colors.astype("float32", copy=False)
elif type(c) == np.ndarray and c.size == v.shape[0]: # Function color
normalize = sh["normalize"][0] != None and sh["normalize"][1] != None
colors = get_colors(c, sh["colormap"], normalize=normalize,
vmin=sh["normalize"][0], vmax=sh["normalize"][1])
colors = colors.astype("float32", copy=False)
# print("Vertex function values")
else:
print("Invalid color array given! Supported are numpy arrays.", type(c))
colors = sh["point_color"]
v_color = False
return colors, v_color
def add_mesh(self, v, f, c=None, uv=None, n=None, shading={}, texture_data=None, **kwargs):
shading.update(kwargs)
sh = self.__get_shading(shading)
mesh_obj = {}
# it is a tet
if v.shape[1] == 3 and f.shape[1] == 4:
f_tmp = np.ndarray([f.shape[0] * 4, 3], dtype=f.dtype)
for i in range(f.shape[0]):
f_tmp[i * 4 + 0] = np.array([f[i][1], f[i][0], f[i][2]])
f_tmp[i * 4 + 1] = np.array([f[i][0], f[i][1], f[i][3]])
f_tmp[i * 4 + 2] = np.array([f[i][1], f[i][2], f[i][3]])
f_tmp[i * 4 + 3] = np.array([f[i][2], f[i][0], f[i][3]])
f = f_tmp
if v.shape[1] == 2:
v = np.append(v, np.zeros([v.shape[0], 1]), 1)
# Type adjustment vertices
v = v.astype("float32", copy=False)
# Color setup
colors, coloring = self.__get_colors(v, f, c, sh)
# Type adjustment faces and colors
c = colors.astype("float32", copy=False)
# Material and geometry setup
ba_dict = {"color": p3s.BufferAttribute(c)}
if coloring == "FaceColors":
verts = np.zeros((f.shape[0] * 3, 3), dtype="float32")
for ii in range(f.shape[0]):
# print(ii*3, f[ii])
verts[ii * 3] = v[f[ii, 0]]
verts[ii * 3 + 1] = v[f[ii, 1]]
verts[ii * 3 + 2] = v[f[ii, 2]]
v = verts
else:
f = f.astype("uint32", copy=False).ravel()
ba_dict["index"] = p3s.BufferAttribute(f, normalized=False)
ba_dict["position"] = p3s.BufferAttribute(v, normalized=False)
if uv is not None:
uv = (uv - np.min(uv)) / (np.max(uv) - np.min(uv))
if texture_data is None:
texture_data = gen_checkers(20, 20)
tex = p3s.DataTexture(data=texture_data, format="RGBFormat", type="FloatType")
material = p3s.MeshStandardMaterial(map=tex, reflectivity=sh["reflectivity"], side=sh["side"],
roughness=sh["roughness"], metalness=sh["metalness"],
flatShading=sh["flat"],
polygonOffset=True, polygonOffsetFactor=1, polygonOffsetUnits=5)
ba_dict["uv"] = p3s.BufferAttribute(uv.astype("float32", copy=False))
else:
material = p3s.MeshStandardMaterial(vertexColors=coloring, reflectivity=sh["reflectivity"],
side=sh["side"], roughness=sh["roughness"], metalness=sh["metalness"],
flatShading=sh["flat"],
polygonOffset=True, polygonOffsetFactor=1, polygonOffsetUnits=5)
if type(n) != type(None) and coloring == "VertexColors": # TODO: properly handle normals for FaceColors as well
ba_dict["normal"] = p3s.BufferAttribute(n.astype("float32", copy=False), normalized=True)
geometry = p3s.BufferGeometry(attributes=ba_dict)
if coloring == "VertexColors" and type(n) == type(None):
geometry.exec_three_obj_method('computeVertexNormals')
elif coloring == "FaceColors" and type(n) == type(None):
geometry.exec_three_obj_method('computeFaceNormals')
# Mesh setup
mesh = p3s.Mesh(geometry=geometry, material=material)
# Wireframe setup
mesh_obj["wireframe"] = None
if sh["wireframe"]:
wf_geometry = p3s.WireframeGeometry(mesh.geometry) # WireframeGeometry
wf_material = p3s.LineBasicMaterial(color=sh["wire_color"], linewidth=sh["wire_width"])
wireframe = p3s.LineSegments(wf_geometry, wf_material)
mesh.add(wireframe)
mesh_obj["wireframe"] = wireframe
# Bounding box setup
if sh["bbox"]:
v_box, f_box = self.__get_bbox(v)
_, bbox = self.add_edges(v_box, f_box, sh, mesh)
mesh_obj["bbox"] = [bbox, v_box, f_box]
# Object setup
mesh_obj["max"] = np.max(v, axis=0)
mesh_obj["min"] = np.min(v, axis=0)
mesh_obj["geometry"] = geometry
mesh_obj["mesh"] = mesh
mesh_obj["material"] = material
mesh_obj["type"] = "Mesh"
mesh_obj["shading"] = sh
mesh_obj["coloring"] = coloring
mesh_obj["arrays"] = [v, f, c] # TODO replays with proper storage or remove if not needed
return self.__add_object(mesh_obj)
def add_lines(self, beginning, ending, shading={}, obj=None, **kwargs):
shading.update(kwargs)
if len(beginning.shape) == 1:
if len(beginning) == 2:
beginning = np.array([[beginning[0], beginning[1], 0]])
else:
if beginning.shape[1] == 2:
beginning = np.append(
beginning, np.zeros([beginning.shape[0], 1]), 1)
if len(ending.shape) == 1:
if len(ending) == 2:
ending = np.array([[ending[0], ending[1], 0]])
else:
if ending.shape[1] == 2:
ending = np.append(
ending, np.zeros([ending.shape[0], 1]), 1)
sh = self.__get_shading(shading)
lines = np.hstack([beginning, ending])
lines = lines.reshape((-1, 3))
return self.__add_line_geometry(lines, sh, obj)
def add_edges(self, vertices, edges, shading={}, obj=None, **kwargs):
shading.update(kwargs)
if vertices.shape[1] == 2:
vertices = np.append(
vertices, np.zeros([vertices.shape[0], 1]), 1)
sh = self.__get_shading(shading)
lines = np.zeros((edges.size, 3))
cnt = 0
for e in edges:
lines[cnt, :] = vertices[e[0]]
lines[cnt + 1, :] = vertices[e[1]]
cnt += 2
return self.__add_line_geometry(lines, sh, obj)
def add_points(self, points, c=None, shading={}, obj=None, **kwargs):
shading.update(kwargs)
if len(points.shape) == 1:
if len(points) == 2:
points = np.array([[points[0], points[1], 0]])
else:
if points.shape[1] == 2:
points = np.append(
points, np.zeros([points.shape[0], 1]), 1)
sh = self.__get_shading(shading)
points = points.astype("float32", copy=False)
mi = np.min(points, axis=0)
ma = np.max(points, axis=0)
g_attributes = {"position": p3s.BufferAttribute(points, normalized=False)}
m_attributes = {"size": sh["point_size"]}
if sh["point_shape"] == "circle": # Plot circles
tex = p3s.DataTexture(data=gen_circle(16, 16), format="RGBAFormat", type="FloatType")
m_attributes["map"] = tex
m_attributes["alphaTest"] = 0.5
m_attributes["transparency"] = True
else: # Plot squares
pass
colors, v_colors = self.__get_point_colors(points, c, sh)
if v_colors: # Colors per point
m_attributes["vertexColors"] = 'VertexColors'
g_attributes["color"] = p3s.BufferAttribute(colors, normalized=False)
else: # Colors for all points
m_attributes["color"] = colors
material = p3s.PointsMaterial(**m_attributes)
geometry = p3s.BufferGeometry(attributes=g_attributes)
points = p3s.Points(geometry=geometry, material=material)
point_obj = {"geometry": geometry, "mesh": points, "material": material,
"max": ma, "min": mi, "type": "Points", "wireframe": None}
if obj:
return self.__add_object(point_obj, obj), point_obj
else:
return self.__add_object(point_obj)
def remove_object(self, obj_id):
if obj_id not in self.__objects:
print("Invalid object id. Valid ids are: ", list(self.__objects.keys()))
return
self._scene.remove(self.__objects[obj_id]["mesh"])
del self.__objects[obj_id]
self.__update_view()
def reset(self):
for obj_id in list(self.__objects.keys()).copy():
self._scene.remove(self.__objects[obj_id]["mesh"])
del self.__objects[obj_id]
self.__update_view()
def update_object(self, oid=0, vertices=None, colors=None, faces=None):
obj = self.__objects[oid]
if type(vertices) != type(None):
if obj["coloring"] == "FaceColors":
f = obj["arrays"][1]
verts = np.zeros((f.shape[0] * 3, 3), dtype="float32")
for ii in range(f.shape[0]):
# print(ii*3, f[ii])
verts[ii * 3] = vertices[f[ii, 0]]
verts[ii * 3 + 1] = vertices[f[ii, 1]]
verts[ii * 3 + 2] = vertices[f[ii, 2]]
v = verts
else:
v = vertices.astype("float32", copy=False)
obj["geometry"].attributes["position"].array = v
# self.wireframe.attributes["position"].array = v # Wireframe updates?
obj["geometry"].attributes["position"].needsUpdate = True
# obj["geometry"].exec_three_obj_method('computeVertexNormals')
if type(colors) != type(None):
colors, coloring = self.__get_colors(obj["arrays"][0], obj["arrays"][1], colors, obj["shading"])
colors = colors.astype("float32", copy=False)
obj["geometry"].attributes["color"].array = colors
obj["geometry"].attributes["color"].needsUpdate = True
if type(faces) != type(None):
if obj["coloring"] == "FaceColors":
print("Face updates are currently only possible in vertex color mode.")
return
f = faces.astype("uint32", copy=False).ravel()
print(obj["geometry"].attributes)
obj["geometry"].attributes["index"].array = f
# self.wireframe.attributes["position"].array = v # Wireframe updates?
obj["geometry"].attributes["index"].needsUpdate = True
# obj["geometry"].exec_three_obj_method('computeVertexNormals')
# self.mesh.geometry.verticesNeedUpdate = True
# self.mesh.geometry.elementsNeedUpdate = True
# self.update()
if self.render_mode == "WEBSITE":
return self
# def update(self):
# self.mesh.exec_three_obj_method('update')
# self.orbit.exec_three_obj_method('update')
# self.cam.exec_three_obj_method('updateProjectionMatrix')
# self.scene.exec_three_obj_method('update')
def add_text(self, text, shading={}, **kwargs):
shading.update(kwargs)
sh = self.__get_shading(shading)
tt = p3s.TextTexture(string=text, color=sh["text_color"])
sm = p3s.SpriteMaterial(map=tt)
text = p3s.Sprite(material=sm, scaleToTexture=True)
self._scene.add(text)
# def add_widget(self, widget, callback):
# self.widgets.append(widget)
# widget.observe(callback, names='value')
# def add_dropdown(self, options, default, desc, cb):
# widget = widgets.Dropdown(options=options, value=default, description=desc)
# self.__widgets.append(widget)
# widget.observe(cb, names="value")
# display(widget)
# def add_button(self, text, cb):
# button = widgets.Button(description=text)
# self.__widgets.append(button)
# button.on_click(cb)
# display(button)
def to_html(self, imports=True, html_frame=True):
# Bake positions (fixes centering bug in offline rendering)
if len(self.__objects) == 0:
return
ma = np.zeros((len(self.__objects), 3))
mi = np.zeros((len(self.__objects), 3))
for r, obj in enumerate(self.__objects):
ma[r] = self.__objects[obj]["max"]
mi[r] = self.__objects[obj]["min"]
ma = np.max(ma, axis=0)
mi = np.min(mi, axis=0)
diag = np.linalg.norm(ma - mi)
mean = (ma - mi) / 2 + mi
for r, obj in enumerate(self.__objects):
v = self.__objects[obj]["geometry"].attributes["position"].array
v -= mean
v += np.array([0.0, .9, 0.0]) #! to move the obj to the center of window
scale = self.__s["scale"] * (diag)
self._orbit.target = [0.0, 0.0, 0.0]
self._cam.lookAt([0.0, 0.0, 0.0])
# self._cam.position = [0.0, 0.0, scale]
self._cam.position = [0.0, 0.5, scale * 1.3] #! show four complete meshes in the window
self._light.position = [0.0, 0.0, scale]
state = embed.dependency_state(self._renderer)
# Somehow these entries are missing when the state is exported in python.
# Exporting from the GUI works, so we are inserting the missing entries.
for k in state:
if state[k]["model_name"] == "OrbitControlsModel":
state[k]["state"]["maxAzimuthAngle"] = "inf"
state[k]["state"]["maxDistance"] = "inf"
state[k]["state"]["maxZoom"] = "inf"
state[k]["state"]["minAzimuthAngle"] = "-inf"
tpl = embed.load_requirejs_template
if not imports:
embed.load_requirejs_template = ""
s = embed.embed_snippet(self._renderer, state=state, embed_url=EMBED_URL)
# s = embed.embed_snippet(self.__w, state=state)
embed.load_requirejs_template = tpl
if html_frame:
s = "<html>\n<body>\n" + s + "\n</body>\n</html>"
# Revert changes
for r, obj in enumerate(self.__objects):
v = self.__objects[obj]["geometry"].attributes["position"].array
v += mean
self.__update_view()
return s
def save(self, filename=""):
if filename == "":
uid = str(uuid.uuid4()) + ".html"
else:
filename = filename.replace(".html", "")
uid = filename + '.html'
with open(uid, "w") as f:
f.write(self.to_html())
print("Plot saved to file %s." % uid)