Recently, I’m learning about Information Geometry, during which I realized that it’s a good idea to use Blender’s Python API to encapsulate a toolset for efficient and elegant visualization of functions and data.

I started from the sign mark – coordinate system. I encapsulated the functionality to a CoordinateSystem class.

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
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
class CoordinateSystem:
"""
A class to create and manage a highly polished 3D coordinate system in Blender.

This version is optimized for performance by using direct mesh creation
and combining all geometry into a single object.
"""

def __init__(
self,
size=10.0,
subdivisions=10,
show_labels=True,
axis_thickness=0.05,
label_offset=0.5,
):
"""
Initializes the coordinate system with given parameters.

Args:
size (float): The length of each axis.
subdivisions (int): The number of divisions for the axes.
show_labels (bool): Whether to create text labels for the axes and ticks.
axis_thickness (float): The thickness of the axes lines.
label_offset (float): The distance of the tick labels from the axis.
"""
self.size = size
self.subdivisions = subdivisions
self.show_labels = show_labels
self.axis_thickness = axis_thickness
self.label_offset = label_offset

self.collection = None
self.materials = {}
self.base_text_obj = None

def create(self):
"""
Main method to create the coordinate system in the Blender scene.
"""
self._clear_scene()
self._create_collection()
self._create_materials()

# This is the key optimization: create a single mesh object for axes and ticks
self._create_combined_geometry()

if self.show_labels:
self._create_labels()

print(
f"Created a polished coordinate system with size={self.size} and {self.subdivisions} subdivisions."
)

def _clear_scene(self):
"""Helper method to clear all objects from the scene."""
bpy.ops.object.select_all(action="SELECT")
bpy.ops.object.delete()

def _create_collection(self):
"""Helper method to create a new collection for the system."""
self.collection = bpy.data.collections.new("Coordinate System")
bpy.context.scene.collection.children.link(self.collection)

def _create_materials(self):
"""Helper method to create and store all necessary materials."""

def create_color_material(name, color):
mat = bpy.data.materials.new(name=name)
mat.use_nodes = True
bsdf = mat.node_tree.nodes.get("Principled BSDF")
if bsdf:
bsdf.inputs["Base Color"].default_value = color
return mat

self.materials["X"] = create_color_material("Red_Axis", (1.0, 0.0, 0.0, 1.0))
self.materials["Y"] = create_color_material("Green_Axis", (0.0, 1.0, 0.0, 1.0))
self.materials["Z"] = create_color_material("Blue_Axis", (0.0, 0.0, 1.0, 1.0))
self.materials["Ticks"] = create_color_material(
"Tick_Mat", (0.8, 0.8, 0.8, 1.0)
)
self.materials["Labels"] = create_color_material(
"Label_Mat", (0.9, 0.9, 0.9, 1.0)
)

def _create_polished_mesh_cylinder(self, bm, matrix, radius, depth):
"""
Helper to create a cylinder directly in a bmesh object with a transform.

Note: The bmesh module uses create_cone with equal radii to create a cylinder.
"""
geom = bmesh.ops.create_cone(
bm,
cap_ends=True,
radius1=radius,
radius2=radius, # This is the crucial fix: both radii must be the same
depth=depth,
segments=8,
)
bmesh.ops.transform(bm, verts=geom["verts"], matrix=matrix)

def _create_combined_geometry(self):
"""
Creates all axes, ticks, and arrows as a single, optimized mesh object.
"""
# Create a new bmesh object to work with
bm = bmesh.new()

# Define axis parameters
axes_data = [
(
"X",
self.materials["X"],
(1, 0, 0),
Matrix.Rotation(math.radians(90), 4, "Y"),
),
(
"Y",
self.materials["Y"],
(0, 1, 0),
Matrix.Rotation(math.radians(-90), 4, "X"),
),
("Z", self.materials["Z"], (0, 0, 1), Matrix.Identity(4)),
]

# Add geometry for each axis and its ticks
for axis_label, mat, vec, mat_rot in axes_data:
# Create the main axis geometry
depth = self.size
if axis_label == "Z": # The default cylinder is aligned with Z
mat_trans = Matrix.Translation((0, 0, depth / 2))
else:
mat_trans = Matrix.Translation(tuple(v * depth / 2 for v in vec))

# Create the axis arrow cone
cone_radius = self.axis_thickness * 2.5
cone_depth = self.axis_thickness * 10
cone_trans = Matrix.Translation(
tuple(v * (depth + cone_depth / 2) for v in vec)
)

# Create axis and arrow as a single piece
self._create_polished_mesh_cylinder(
bm, mat_trans @ mat_rot, self.axis_thickness, depth
)
bmesh.ops.create_cone(
bm,
cap_ends=True,
radius1=cone_radius,
radius2=0,
depth=cone_depth,
segments=8,
matrix=cone_trans @ mat_rot,
)

# Create tick marks
for i in range(1, self.subdivisions + 1):
val = self.size / self.subdivisions * i
tick_loc = tuple(v * val for v in vec)
tick_matrix = Matrix.Translation(tick_loc) @ mat_rot
self._create_polished_mesh_cylinder(bm, tick_matrix, 0.01, 0.2)

# Create a single mesh from the bmesh data
mesh = bpy.data.meshes.new("CoordinateSystem_Mesh")
bm.to_mesh(mesh)
bm.free()

# Create the final object
obj = bpy.data.objects.new("CoordinateSystem", mesh)
self.collection.objects.link(obj)

# Create the final object
obj = bpy.data.objects.new("CoordinateSystem", mesh)
self.collection.objects.link(obj)

# 安全地设置活动对象
if bpy.context.view_layer is not None:
try:
bpy.context.view_layer.objects.active = obj
obj.select_set(True)
except AttributeError:
# 如果view_layer不可用,使用场景的主集合作为备选方案
scene = bpy.context.scene
if scene is not None and scene.collection is not None:
scene.collection.objects.link(obj)
# 确保更新场景
if bpy.context.view_layer is not None:
bpy.context.view_layer.update()

# Assign materials to the mesh
if obj.data:
obj.data.materials.append(self.materials["X"])
obj.data.materials.append(self.materials["Y"])
obj.data.materials.append(self.materials["Z"])
obj.data.materials.append(self.materials["Ticks"])

# (Optional) Assign different materials to different parts of the mesh
# This is more complex and left out for brevity, but is possible by
# assigning material indices to faces in the bmesh step.

def _create_labels(self):
"""Helper method to create all text labels for the axes."""
# Create a basic text object to copy from
bpy.ops.object.text_add(enter_editmode=False)
self.base_text_obj = bpy.context.active_object
self.base_text_obj.data.materials.append(self.materials["Labels"])
self.base_text_obj.data.size = 0.5
bpy.context.collection.objects.unlink(self.base_text_obj)

# Define axis parameters in a list of tuples for better organization
axes_data = [
("X", (1, 0, 0), (0, -self.label_offset, 0)),
("Y", (0, 1, 0), (-self.label_offset, 0, 0)),
("Z", (0, 0, 1), (0, -self.label_offset, self.label_offset)),
]

for axis_label, tick_loc_vec, label_offset_vec in axes_data:
for i in range(1, self.subdivisions + 1):
val = round(self.size / self.subdivisions * i, 2)

label_obj = self.base_text_obj.copy()
label_obj.data = self.base_text_obj.data.copy()
label_obj.data.body = str(val)
label_obj.location = tuple(
v * val + o for v, o in zip(tick_loc_vec, label_offset_vec)
)
label_obj.name = f"Label_{axis_label}_{val}"
self.collection.objects.link(label_obj)

# Cleanup: delete the base text object
bpy.data.objects.remove(self.base_text_obj)
self.base_text_obj = None

This can generate a simple 3d coordinate frame now, despite having some problems to tackle. The frame includes the coordinate axis, arrows, and tick numbers. But I need to solve the overlapping of numbers on axis in the future, then enhance the functionality and flexibility of the class.

Flaws are where the revolutions appear.