fix(hair): cards follow the scalp normal, three overlapping rows
The first card build anchored every card on a fixed outward vector, so the silhouette was a spiked crown. Each card is now oriented from the scalp normal at its anchor: the length runs along the downward tangent, the width across it, and a small tip lift gives the cut volume. Three overlapping rows (7/9/10) replace the single ring; overlap is what stops cards reading as separate spikes. 26 cards, 401 verts, 224 quads, 468 tris, still all quads on one head bone. Recorded as still open: the skin renders darker than the baked diffuse suggests, and the generated pore and strand textures remain unwired because the material-editing tool segfaults in the headless commandlet.
This commit is contained in:
1 parent
39f07b0f5f
commit
1fc5835327
39 files changed
+84
-68
No files matched your search
@@ -69,8 +69,14 @@ def hair_material() -> bpy.types.Material:
|
||||
return mat
|
||||
|
||||
|
||||
def card(name, anchor, outward, up, width, height, curl, segments=4):
|
||||
"""One curved card: a strip of quads that arcs away from the scalp."""
|
||||
def card(name, anchor, along, across, normal, width, height, lift, segments=4):
|
||||
"""A card that lies ALONG the scalp rather than sticking out of it.
|
||||
|
||||
`along` is the direction the hair falls over the skull (the downward tangent at
|
||||
the anchor), `across` the card's width direction and `normal` the scalp normal.
|
||||
The first version passed a fixed global up and an outward vector, which made every
|
||||
card stand away from the head - the spiked crown in the review shot.
|
||||
"""
|
||||
mesh = bpy.data.meshes.new(name)
|
||||
obj = bpy.data.objects.new(name, mesh)
|
||||
bpy.context.scene.collection.objects.link(obj)
|
||||
@@ -79,13 +85,11 @@ def card(name, anchor, outward, up, width, height, curl, segments=4):
|
||||
faces = []
|
||||
for i in range(segments + 1):
|
||||
t = i / segments
|
||||
# Taper to a point at the tip and curl outward as the strand falls.
|
||||
w = width * (1.0 - 0.75 * t)
|
||||
h = height * t
|
||||
bulge = curl * math.sin(t * math.pi * 0.85)
|
||||
base = anchor + up * h + outward * bulge
|
||||
left = base - outward.cross(up).normalized() * w
|
||||
right = base + outward.cross(up).normalized() * w
|
||||
w = width * (1.0 - 0.65 * t)
|
||||
# Follow the skull, then lift a little at the tip so the cut has volume.
|
||||
base = anchor + along * (height * t) + normal * (lift * math.sin(t * math.pi * 0.7))
|
||||
left = base - across * w
|
||||
right = base + across * w
|
||||
verts.append(left)
|
||||
verts.append(right)
|
||||
for i in range(segments):
|
||||
@@ -133,21 +137,29 @@ def main(argv=None) -> int:
|
||||
material = hair_material()
|
||||
pieces = [scalp_cap(Vector((centre.x, centre.y, centre.z + 0.012)), radius, material)]
|
||||
|
||||
up = Vector((0.0, 0.0, 1.0))
|
||||
rings = ((CARDS_INNER, 1.00, 0.70), (CARDS_MIDDLE, 0.90, 0.85), (CARDS_OUTER, 0.78, 1.0))
|
||||
down = Vector((0.0, 0.0, -1.0))
|
||||
# Two rows: an inner row that covers the crown and an outer row that overlaps it
|
||||
# down to the hairline. Overlap is what stops the cards reading as separate spikes.
|
||||
rings = (
|
||||
# count, radius scale, anchor height, length, width, lift
|
||||
(7, 0.55, 0.80, 0.085, 0.30, 0.25),
|
||||
(9, 0.72, 0.62, 0.070, 0.26, 0.18),
|
||||
(10, 0.86, 0.42, 0.055, 0.24, 0.12),
|
||||
)
|
||||
index = 0
|
||||
for ring, (count, radius_scale, length_scale) in enumerate(rings):
|
||||
for ring, (count, radius_scale, height_scale, length, width_scale, lift_scale) in enumerate(rings):
|
||||
for i in range(count):
|
||||
angle = math.tau * (i + 0.5 * ring) / count
|
||||
outward = Vector((math.cos(angle), math.sin(angle), 0.22)).normalized()
|
||||
anchor = Vector((
|
||||
centre.x + outward.x * radius * radius_scale * 0.92,
|
||||
centre.y + outward.y * radius * radius_scale * 0.92,
|
||||
centre.z + radius * (0.52 - 0.16 * ring),
|
||||
))
|
||||
pieces.append(card(f"HairCard_{index}", anchor, outward, up,
|
||||
width=radius * 0.30, height=CARD_HEIGHT * length_scale,
|
||||
curl=radius * 0.34))
|
||||
horizontal = Vector((math.cos(angle), math.sin(angle), 0.0))
|
||||
normal = (horizontal * radius_scale + Vector((0.0, 0.0, height_scale))).normalized()
|
||||
anchor = Vector((centre.x, centre.y, centre.z)) + normal * radius
|
||||
# Downward tangent: the part of -Z that lies in the scalp's tangent plane.
|
||||
along = (down - normal * down.dot(normal)).normalized()
|
||||
across = along.cross(normal).normalized()
|
||||
pieces.append(card(f"HairCard_{index}", anchor, along, across, normal,
|
||||
width=radius * width_scale,
|
||||
height=radius * length * 10.0,
|
||||
lift=radius * lift_scale))
|
||||
index += 1
|
||||
log(f"built {len(pieces)} pieces: 1 scalp cap + {index} cards")
|
||||
|
||||
|
||||
Reference in new issue
Block a user