highly improved primitive building
This commit is contained in:
parent
84fd407c5b
commit
606df32fcb
193
__init__.py
193
__init__.py
@ -5,35 +5,59 @@ from mathutils import Vector, Matrix
|
|||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Canonical primitive builders (Z-up, origin at centre / base)
|
# Lookup tables
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
def _sphere_positions(radius: float, segments: int) -> list[Vector]:
|
def _build_unit_circle(segments: int) -> list:
|
||||||
|
"""(cos, sin) sampled once per segment. Reused by all primitives."""
|
||||||
|
return [
|
||||||
|
(math.cos(2 * math.pi * i / segments),
|
||||||
|
math.sin(2 * math.pi * i / segments))
|
||||||
|
for i in range(segments)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _build_latitude_stack(segments: int) -> list:
|
||||||
|
"""(sin_phi, cos_phi) for each latitude ring, poles included."""
|
||||||
rings = max(segments // 2, 2)
|
rings = max(segments // 2, 2)
|
||||||
|
return [
|
||||||
|
(math.sin(math.pi * r / rings),
|
||||||
|
math.cos(math.pi * r / rings))
|
||||||
|
for r in range(rings + 1)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class _Tables:
|
||||||
|
"""Precomputed trig tables for a given segment count."""
|
||||||
|
def __init__(self, segments: int):
|
||||||
|
self.segments = segments
|
||||||
|
self.rings = max(segments // 2, 2)
|
||||||
|
self.circle = _build_unit_circle(segments)
|
||||||
|
self.latitudes = _build_latitude_stack(segments)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Canonical primitive builders (Z-up, origin at centre)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _sphere_positions(radius: float, tables: _Tables) -> list:
|
||||||
out = []
|
out = []
|
||||||
for r in range(rings + 1):
|
for sin_phi, cos_phi in tables.latitudes:
|
||||||
phi = math.pi * r / rings
|
for c, s in tables.circle:
|
||||||
sp, cp = math.sin(phi), math.cos(phi)
|
out.append(Vector((
|
||||||
for s in range(segments):
|
radius * sin_phi * c,
|
||||||
theta = 2.0 * math.pi * s / segments
|
radius * sin_phi * s,
|
||||||
out.append(Vector((radius * sp * math.cos(theta),
|
radius * cos_phi,
|
||||||
radius * sp * math.sin(theta),
|
)))
|
||||||
radius * cp)))
|
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
def _ring_positions(radius: float, segments: int) -> list[Vector]:
|
def _ring_positions(radius: float, tables: _Tables) -> list:
|
||||||
"""Unit ring in the XY plane — the caller's matrix orients it."""
|
"""Flat ring in the XY plane — caller's matrix orients it."""
|
||||||
out = []
|
return [Vector((c * radius, s * radius, 0.0)) for c, s in tables.circle]
|
||||||
for i in range(segments):
|
|
||||||
theta = 2.0 * math.pi * i / segments
|
|
||||||
out.append(Vector((math.cos(theta) * radius,
|
|
||||||
math.sin(theta) * radius,
|
|
||||||
0.0)))
|
|
||||||
return out
|
|
||||||
|
|
||||||
|
|
||||||
def _apply(mat: Matrix, vecs: list[Vector]) -> list[Vector]:
|
def _apply(mat: Matrix, vecs: list) -> list:
|
||||||
return [(mat @ v.to_4d()).to_3d() for v in vecs]
|
return [(mat @ v.to_4d()).to_3d() for v in vecs]
|
||||||
|
|
||||||
|
|
||||||
@ -41,18 +65,21 @@ def _apply(mat: Matrix, vecs: list[Vector]) -> list[Vector]:
|
|||||||
# BMesh helpers
|
# BMesh helpers
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
def add_sphere(bm: bmesh.types.BMesh, mat: Matrix, radius: float, segments: int):
|
def add_sphere(bm: bmesh.types.BMesh, mat: Matrix, radius: float, tables: _Tables):
|
||||||
"""Sphere built at origin/Z-up, then transformed by `mat`."""
|
"""Sphere built at origin/Z-up, then placed by `mat`."""
|
||||||
rings = max(segments // 2, 2)
|
segs = tables.segments
|
||||||
verts = [bm.verts.new(p) for p in _apply(mat, _sphere_positions(radius, segments))]
|
rings = tables.rings
|
||||||
|
verts = [bm.verts.new(p) for p in _apply(mat, _sphere_positions(radius, tables))]
|
||||||
for r in range(rings):
|
for r in range(rings):
|
||||||
for s in range(segments):
|
for s in range(segs):
|
||||||
sn = (s + 1) % segments
|
sn = (s + 1) % segs
|
||||||
try:
|
try:
|
||||||
bm.faces.new((verts[ r * segments + s],
|
bm.faces.new((
|
||||||
verts[ r * segments + sn],
|
verts[ r * segs + s ],
|
||||||
verts[(r + 1) * segments + sn],
|
verts[ r * segs + sn],
|
||||||
verts[(r + 1) * segments + s]))
|
verts[(r + 1) * segs + sn],
|
||||||
|
verts[(r + 1) * segs + s ],
|
||||||
|
))
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -60,40 +87,41 @@ def add_sphere(bm: bmesh.types.BMesh, mat: Matrix, radius: float, segments: int)
|
|||||||
def add_capped_frustum(bm: bmesh.types.BMesh,
|
def add_capped_frustum(bm: bmesh.types.BMesh,
|
||||||
head_mat: Matrix, head_radius: float,
|
head_mat: Matrix, head_radius: float,
|
||||||
tail_mat: Matrix, tail_radius: float,
|
tail_mat: Matrix, tail_radius: float,
|
||||||
segments: int):
|
tables: _Tables):
|
||||||
"""
|
"""
|
||||||
Tapered cylinder. Each ring is built canonically in XY then placed by
|
Tapered cylinder. Rings are built in XY then placed by per-end matrices —
|
||||||
the per-end matrix — no axis arithmetic at all.
|
no axis arithmetic needed.
|
||||||
head_mat / tail_mat each encode: orientation (bone axes) + translation
|
|
||||||
(the head or tail world position). The ring normal naturally aligns with
|
|
||||||
the bone axis because the matrix was built that way.
|
|
||||||
"""
|
"""
|
||||||
head_pos = (head_mat @ Vector((0, 0, 0, 1))).to_3d()
|
head_pos = (head_mat @ Vector((0, 0, 0, 1))).to_3d()
|
||||||
tail_pos = (tail_mat @ Vector((0, 0, 0, 1))).to_3d()
|
tail_pos = (tail_mat @ Vector((0, 0, 0, 1))).to_3d()
|
||||||
if (tail_pos - head_pos).length < 1e-6:
|
if (tail_pos - head_pos).length < 1e-6:
|
||||||
return
|
return
|
||||||
|
|
||||||
hr = [bm.verts.new(p) for p in _apply(head_mat, _ring_positions(head_radius, segments))]
|
segs = tables.segments
|
||||||
tr = [bm.verts.new(p) for p in _apply(tail_mat, _ring_positions(tail_radius, segments))]
|
hr = [bm.verts.new(p) for p in _apply(head_mat, _ring_positions(head_radius, tables))]
|
||||||
|
tr = [bm.verts.new(p) for p in _apply(tail_mat, _ring_positions(tail_radius, tables))]
|
||||||
|
|
||||||
for i in range(segments):
|
# Side quads
|
||||||
nxt = (i + 1) % segments
|
for i in range(segs):
|
||||||
|
nxt = (i + 1) % segs
|
||||||
try:
|
try:
|
||||||
bm.faces.new((hr[i], hr[nxt], tr[nxt], tr[i]))
|
bm.faces.new((hr[i], hr[nxt], tr[nxt], tr[i]))
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# Head cap
|
||||||
hcv = bm.verts.new(head_pos)
|
hcv = bm.verts.new(head_pos)
|
||||||
for i in range(segments):
|
for i in range(segs):
|
||||||
try:
|
try:
|
||||||
bm.faces.new((hcv, hr[(i + 1) % segments], hr[i]))
|
bm.faces.new((hcv, hr[(i + 1) % segs], hr[i]))
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# Tail cap
|
||||||
tcv = bm.verts.new(tail_pos)
|
tcv = bm.verts.new(tail_pos)
|
||||||
for i in range(segments):
|
for i in range(segs):
|
||||||
try:
|
try:
|
||||||
bm.faces.new((tcv, tr[i], tr[(i + 1) % segments]))
|
bm.faces.new((tcv, tr[i], tr[(i + 1) % segs]))
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -104,28 +132,25 @@ def add_capped_frustum(bm: bmesh.types.BMesh,
|
|||||||
|
|
||||||
def _end_matrices(world_mat: Matrix, bone_matrix: Matrix, bone_length: float):
|
def _end_matrices(world_mat: Matrix, bone_matrix: Matrix, bone_length: float):
|
||||||
"""
|
"""
|
||||||
Return (head_mat, tail_mat) — 4x4 world-space matrices for each bone end.
|
Return (head_mat, tail_mat) — world-space matrices for each bone end.
|
||||||
|
|
||||||
The bone matrix (armature-local) has:
|
The bone matrix (armature-local) columns:
|
||||||
col[0] = bone X axis
|
col[0] = bone X axis
|
||||||
col[1] = bone Y axis ← along the bone
|
col[1] = bone Y axis ← along the bone
|
||||||
col[2] = bone Z axis
|
col[2] = bone Z axis
|
||||||
col[3] = head position (armature local)
|
col[3] = head position
|
||||||
|
|
||||||
We want each end's matrix to have:
|
We remap columns so our local Z = bone Y, meaning _ring_positions()
|
||||||
our X = bone X ← spans the cross-section ring
|
(which lies in XY, normal = +Z) is automatically perpendicular to the
|
||||||
our Y = bone Z ← spans the cross-section ring
|
bone — no cross products or acos required.
|
||||||
our Z = bone Y ← ring normal points along bone
|
our X = bone X
|
||||||
translation = head or tail world position
|
our Y = bone Z
|
||||||
|
our Z = bone Y (ring normal → along bone)
|
||||||
This means _ring_positions() (which lies in XY, normal=Z) will be
|
|
||||||
perpendicular to the bone with zero trigonometry.
|
|
||||||
"""
|
"""
|
||||||
bx = bone_matrix.col[0].to_3d()
|
bx = bone_matrix.col[0].to_3d()
|
||||||
by = bone_matrix.col[1].to_3d() # along bone
|
by = bone_matrix.col[1].to_3d() # along bone
|
||||||
bz = bone_matrix.col[2].to_3d()
|
bz = bone_matrix.col[2].to_3d()
|
||||||
|
|
||||||
# 3x3 orientation: columns = [our_X, our_Y, our_Z] = [bx, bz, by]
|
|
||||||
orient = Matrix((
|
orient = Matrix((
|
||||||
(bx.x, bz.x, by.x),
|
(bx.x, bz.x, by.x),
|
||||||
(bx.y, bz.y, by.y),
|
(bx.y, bz.y, by.y),
|
||||||
@ -136,12 +161,8 @@ def _end_matrices(world_mat: Matrix, bone_matrix: Matrix, bone_length: float):
|
|||||||
tail_local = head_local + by * bone_length
|
tail_local = head_local + by * bone_length
|
||||||
|
|
||||||
def _make(pos_local):
|
def _make(pos_local):
|
||||||
m = orient.to_4x4()
|
|
||||||
world_pos = (world_mat @ pos_local.to_4d()).to_3d()
|
|
||||||
# orientation columns also need the world rotation/scale applied
|
|
||||||
# Easiest: build full local matrix then left-multiply by world_mat.
|
|
||||||
local_mat = orient.to_4x4()
|
local_mat = orient.to_4x4()
|
||||||
local_mat.col[3][:3] = pos_local # set translation in armature space
|
local_mat.col[3][:3] = pos_local
|
||||||
return world_mat @ local_mat
|
return world_mat @ local_mat
|
||||||
|
|
||||||
return _make(head_local), _make(tail_local)
|
return _make(head_local), _make(tail_local)
|
||||||
@ -172,22 +193,22 @@ class ARMATURE_OT_build_envelope_mesh(bpy.types.Operator):
|
|||||||
return obj is not None and obj.type == 'ARMATURE'
|
return obj is not None and obj.type == 'ARMATURE'
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
arm_obj = context.active_object
|
arm_obj = context.active_object
|
||||||
original_mode = arm_obj.mode
|
world_mat = arm_obj.matrix_world
|
||||||
world_mat = arm_obj.matrix_world
|
bm = bmesh.new()
|
||||||
bm = bmesh.new()
|
tables = _Tables(self.segments)
|
||||||
segs = self.segments
|
|
||||||
|
|
||||||
def meshify(bone_matrix, bone_length, head_radius, tail_radius, draw_head=True):
|
def meshify(bone_matrix, bone_length, head_radius, tail_radius, draw_head=True):
|
||||||
head_mat, tail_mat = _end_matrices(world_mat, bone_matrix, bone_length)
|
head_mat, tail_mat = _end_matrices(world_mat, bone_matrix, bone_length)
|
||||||
axis_vec = (tail_mat @ Vector((0,0,0,1))).to_3d() - (head_mat @ Vector((0,0,0,1))).to_3d()
|
|
||||||
if draw_head:
|
if draw_head:
|
||||||
add_sphere(bm, head_mat, max(head_radius, 0.001), segs)
|
add_sphere(bm, head_mat, max(head_radius, 0.001), tables)
|
||||||
add_sphere(bm, tail_mat, max(tail_radius, 0.001), segs)
|
add_sphere(bm, tail_mat, max(tail_radius, 0.001), tables)
|
||||||
add_capped_frustum(bm, head_mat, max(head_radius, 0.001),
|
add_capped_frustum(bm,
|
||||||
tail_mat, max(tail_radius, 0.001), segs)
|
head_mat, max(head_radius, 0.001),
|
||||||
|
tail_mat, max(tail_radius, 0.001),
|
||||||
|
tables)
|
||||||
|
|
||||||
if original_mode == 'EDIT':
|
if arm_obj.mode == 'EDIT':
|
||||||
for bone in arm_obj.data.edit_bones:
|
for bone in arm_obj.data.edit_bones:
|
||||||
meshify(bone.matrix, bone.length,
|
meshify(bone.matrix, bone.length,
|
||||||
bone.head_radius, bone.tail_radius,
|
bone.head_radius, bone.tail_radius,
|
||||||
@ -198,9 +219,6 @@ class ARMATURE_OT_build_envelope_mesh(bpy.types.Operator):
|
|||||||
bone.bone.head_radius, bone.bone.tail_radius,
|
bone.bone.head_radius, bone.bone.tail_radius,
|
||||||
bone.parent is None)
|
bone.parent is None)
|
||||||
|
|
||||||
if original_mode != 'OBJECT':
|
|
||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
|
||||||
|
|
||||||
mesh = bpy.data.meshes.new(arm_obj.name + "_envelope_mesh")
|
mesh = bpy.data.meshes.new(arm_obj.name + "_envelope_mesh")
|
||||||
bm.to_mesh(mesh)
|
bm.to_mesh(mesh)
|
||||||
bm.free()
|
bm.free()
|
||||||
@ -209,18 +227,6 @@ class ARMATURE_OT_build_envelope_mesh(bpy.types.Operator):
|
|||||||
result_obj = bpy.data.objects.new(arm_obj.name + "_envelope", mesh)
|
result_obj = bpy.data.objects.new(arm_obj.name + "_envelope", mesh)
|
||||||
context.collection.objects.link(result_obj)
|
context.collection.objects.link(result_obj)
|
||||||
|
|
||||||
bpy.ops.object.select_all(action='DESELECT')
|
|
||||||
result_obj.select_set(True)
|
|
||||||
context.view_layer.objects.active = result_obj
|
|
||||||
|
|
||||||
if original_mode == 'EDIT':
|
|
||||||
result_obj.select_set(False)
|
|
||||||
arm_obj.select_set(True)
|
|
||||||
context.view_layer.objects.active = arm_obj
|
|
||||||
bpy.ops.object.mode_set(mode='EDIT')
|
|
||||||
|
|
||||||
source_label = {'OBJECT': "rest pose", 'POSE': "current pose", 'EDIT': "edit-bone layout"}.get(original_mode, original_mode)
|
|
||||||
self.report({'INFO'}, f"Created '{result_obj.name}' from {source_label}.")
|
|
||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
|
||||||
@ -237,8 +243,11 @@ class VIEW3D_PT_armature_mesher(bpy.types.Panel):
|
|||||||
|
|
||||||
def draw(self, context):
|
def draw(self, context):
|
||||||
layout = self.layout
|
layout = self.layout
|
||||||
op = layout.operator(ARMATURE_OT_build_envelope_mesh.bl_idname,
|
op = layout.operator(
|
||||||
text="Build Envelope Mesh", icon='OUTLINER_OB_MESH')
|
ARMATURE_OT_build_envelope_mesh.bl_idname,
|
||||||
|
text="Build Envelope Mesh",
|
||||||
|
icon='OUTLINER_OB_MESH',
|
||||||
|
)
|
||||||
op.segments = 16
|
op.segments = 16
|
||||||
|
|
||||||
|
|
||||||
@ -249,10 +258,12 @@ class VIEW3D_PT_armature_mesher(bpy.types.Panel):
|
|||||||
classes = (ARMATURE_OT_build_envelope_mesh, VIEW3D_PT_armature_mesher)
|
classes = (ARMATURE_OT_build_envelope_mesh, VIEW3D_PT_armature_mesher)
|
||||||
|
|
||||||
def register():
|
def register():
|
||||||
for cls in classes: bpy.utils.register_class(cls)
|
for cls in classes:
|
||||||
|
bpy.utils.register_class(cls)
|
||||||
|
|
||||||
def unregister():
|
def unregister():
|
||||||
for cls in reversed(classes): bpy.utils.unregister_class(cls)
|
for cls in reversed(classes):
|
||||||
|
bpy.utils.unregister_class(cls)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
register()
|
register()
|
||||||
Loading…
Reference in New Issue
Block a user