-
Notifications
You must be signed in to change notification settings - Fork 62
/
Copy pathrenderer.py
179 lines (144 loc) · 5.98 KB
/
renderer.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
"""Export wrapper for POV-Ray.
For creating publication quality plots.
"""
from __future__ import annotations
import os
import shutil
import subprocess
from pathlib import Path
from tempfile import TemporaryDirectory
from typing import ClassVar
from warnings import warn
import numpy as np
from jinja2 import Environment # TODO: add to requirements
from matplotlib.colors import to_hex
from crystal_toolkit.core.scene import Cylinders, Lines, Primitive, Scene, Spheres
from crystal_toolkit.settings import MODULE_PATH, SETTINGS
class POVRayRenderer:
"""A class to interface with the POV-Ray command line tool (ray tracer)."""
_TEMPLATES: ClassVar[dict[str, str]] = {
path.stem: path.read_text()
for path in (MODULE_PATH / "helpers" / "povray" / "templates").glob("*")
}
_ENV: ClassVar[Environment] = Environment()
@staticmethod
def write_scene_to_file(scene: Scene, filename: str | Path):
"""Render a Scene to a PNG file using POV-Ray."""
current_dir = Path.cwd()
with TemporaryDirectory() as temp_dir:
os.chdir(temp_dir)
POVRayRenderer.write_povray_input_scene_and_settings(
scene, image_filename="crystal_toolkit_scene.png"
)
POVRayRenderer.call_povray()
shutil.copy("crystal_toolkit_scene.png", current_dir / filename)
os.chdir(current_dir)
@staticmethod
def call_povray(
povray_args: tuple[str] = ("render.ini",),
povray_path: str = SETTINGS.POVRAY_PATH,
):
"""
Run POV-Ray. Prefer `render_scene` method unless advanced user.
"""
povray_args = [povray_path, *povray_args]
result = subprocess.run(
povray_args, capture_output=True, text=True, check=False
)
if result.returncode != 0:
raise RuntimeError(
f"{povray_path} exit code: {result.returncode}."
f"Please check your POV-Ray installation."
f"\nStdout:\n\n{result.stdout}\n\nStderr:\n\n{result.stderr}"
)
@staticmethod
def write_povray_input_scene_and_settings(
scene,
scene_filename="crystal_toolkit_scene.pov",
settings_filename="render.ini",
image_filename="crystal_toolkit_scene.png",
):
"""
Prefer `render_scene` method unless advanced user.
"""
with open(scene_filename, "w") as f:
scene_str = POVRayRenderer.scene_to_povray(scene)
f.write(POVRayRenderer._TEMPLATES["header"])
f.write(POVRayRenderer._get_camera_for_scene(scene))
f.write(POVRayRenderer._TEMPLATES["lights"])
f.write(scene_str)
render_settings = POVRayRenderer._ENV.from_string(
POVRayRenderer._TEMPLATES["render"]
).render(filename=scene_filename, image_filename=image_filename)
with open(settings_filename, "w") as f:
f.write(render_settings)
@staticmethod
def scene_to_povray(scene: Scene) -> str:
povray_str = ""
for item in scene.contents:
if isinstance(item, Primitive):
povray_str += POVRayRenderer.primitive_to_povray(obj=item)
elif isinstance(item, Scene):
povray_str += POVRayRenderer.scene_to_povray(scene=item)
return povray_str
@staticmethod
def primitive_to_povray(obj: Primitive) -> str:
vect = "{:.4f},{:.4f},{:.4f}"
if isinstance(obj, Spheres):
positions = obj.positions
positions = [vect.format(*pos) for pos in positions]
color = POVRayRenderer._format_color_to_povray(obj.color)
return POVRayRenderer._ENV.from_string(
POVRayRenderer._TEMPLATES["sphere"]
).render(positions=positions, radius=obj.radius, color=color)
elif isinstance(obj, Cylinders):
position_pairs = [
[vect.format(*ipos), vect.format(*fpos)]
for ipos, fpos in obj.positionPairs
]
color = POVRayRenderer._format_color_to_povray(obj.color)
return POVRayRenderer._ENV.from_string(
POVRayRenderer._TEMPLATES["cylinder"]
).render(posPairs=position_pairs, color=color)
elif isinstance(obj, Lines):
pos1, pos2 = (
obj.positions[0::2],
obj.positions[1::2],
)
cylCaps = {tuple(pos) for pos in obj.positions}
cylCaps = [vect.format(*pos) for pos in cylCaps]
position_pairs = [
[vect.format(*ipos), vect.format(*fpos)]
for ipos, fpos in zip(pos1, pos2)
]
return POVRayRenderer._ENV.from_string(
POVRayRenderer._TEMPLATES["line"]
).render(posPairs=position_pairs, cylCaps=cylCaps)
elif isinstance(obj, Primitive):
warn(
f"Skipping {type(obj)}, not yet implemented. Submit PR to add support."
)
return ""
@staticmethod
def _format_color_to_povray(color: str) -> str:
"""Convert a matplotlib-compatible color string to a POV-Ray color string."""
vect = "{:.4f},{:.4f},{:.4f}"
color = to_hex(color)
color = color.replace("#", "")
color = tuple(int(color[i : i + 2], 16) / 255.0 for i in (0, 2, 4))
return f"rgb<{vect.format(*color)}>"
@staticmethod
def _get_camera_for_scene(scene: Scene) -> str:
"""Creates a camera in POV-Ray format for a given scene with respect to its bounding box."""
bounding_box = scene.bounding_box # format is [min_corner, max_corner]
center = (np.array(bounding_box[0]) + bounding_box[1]) / 2
size = np.array(bounding_box[1]) - bounding_box[0]
camera_pos = center + np.array([0, 0, 1.2 * size[2]])
return f"""
camera {{
orthographic
location <{camera_pos[0]:.4f}, {camera_pos[1]:.4f}, {camera_pos[2]:.4f}>
look_at <{center[0]:.4f}, {center[1]:.4f}, {center[2]:.4f}>
sky <0, 0, 1>
}}
"""