"""Blender 5.1: sculpt the modular woodland traveler kit and editable preview. Run Blender --background --python tools/art/build_character_kit.py. Coordinates are Godot Y-up, facing +Z. Every mesh has an explicit socket origin. UV2.x carries a palette role; vertex color is the original Blender preview paint. The Stout shape key changes the lower torso, including clothing and bag straps. """ import json import math from pathlib import Path import bpy from mathutils import Vector ROOT = Path(__file__).resolve().parents[2] SOURCE = ROOT / "art/characters" OUTPUT = ROOT / "assets/characters" SOURCE.mkdir(parents=True, exist_ok=True) OUTPUT.mkdir(parents=True, exist_ok=True) (SOURCE / ".gdignore").write_text("") PALETTE = { "fur": (1, "c78a4e"), "muzzle": (2, "eee4d1"), "coat": (3, "496a9d"), "linen": (4, "efe6d4"), "scarf": (5, "7295bf"), "trousers": (6, "7995b6"), "leather": (7, "6f4937"), "fur_shadow": (8, "805032"), "coat_shadow": (9, "304766"), "ink": (0, "3e352d"), "pink": (0, "bd8674"), "brass": (0, "b99c70"), "ivory": (0, "f9f1dc"), "iris": (0, "6d8999"), } PIECES = {} LIBRARY = {} PIVOTS = {} def coord(point): return Vector((point[0], -point[2], point[1])) def linear(hex_color): channels = [int(hex_color[i:i + 2], 16) / 255 for i in (0, 2, 4)] return tuple(v / 12.92 if v <= 0.04045 else ((v + 0.055) / 1.055) ** 2.4 for v in channels) def paint(obj, part, role): role_id, color = PALETTE[role] rgb = linear(color) attr = obj.data.color_attributes.new(name="Color", type="FLOAT_COLOR", domain="CORNER") obj.data.uv_layers.new(name="UVMap") role_uv = obj.data.uv_layers.new(name="PaletteRole") for poly in obj.data.polygons: poly.use_smooth = True for index in poly.loop_indices: attr.data[index].color = (*rgb, 1.0) role_uv.data[index].uv = (role_id, 0) obj.data.materials.append(bpy.data.materials["TravelerPalette"]) PIECES.setdefault(part, []).append(obj) return obj def mesh_object(part, vertices, faces, role): mesh = bpy.data.meshes.new(part + "_surface") mesh.from_pydata([coord(v) for v in vertices], [], faces) mesh.update() obj = bpy.data.objects.new(part + "_detail", mesh) bpy.context.collection.objects.link(obj) return paint(obj, part, role) def ellipsoid(part, center, radius, role, segments=20, rings=12): bpy.ops.mesh.primitive_uv_sphere_add(segments=segments, ring_count=rings, location=coord(center)) obj = bpy.context.object obj.scale = (radius[0], radius[2], radius[1]) return paint(obj, part, role) def rounded_box(part, center, size, radius, role, tilt=0): bpy.ops.mesh.primitive_cube_add(size=1, location=coord(center)) obj = bpy.context.object obj.scale = (size[0], size[2], size[1]) bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) bevel = obj.modifiers.new("Tailored rounded corners", "BEVEL") bevel.width = radius bevel.segments = 3 bpy.ops.object.modifier_apply(modifier=bevel.name) obj.rotation_euler.z = math.radians(tilt) return paint(obj, part, role) def tube(part, points, radii, role, sides=8, flatten=1.0): verts, faces = [], [] for i, point in enumerate(points): p = Vector(point) tangent = Vector(points[min(i + 1, len(points) - 1)]) - Vector(points[max(0, i - 1)]) tangent.normalize() side = tangent.cross(Vector((0, 0, 1))) if side.length < 0.01: side = tangent.cross(Vector((1, 0, 0))) side.normalize() other = tangent.cross(side).normalized() radius = radii[i] if isinstance(radii, (list, tuple)) else radii for j in range(sides): a = math.tau * j / sides verts.append(p + radius * (math.cos(a) * side + math.sin(a) * other * flatten)) if i: for j in range(sides): a, b = (i - 1) * sides + j, (i - 1) * sides + (j + 1) % sides faces.append((a, b, b + sides, a + sides)) faces.extend([tuple(reversed(range(sides))), tuple(range(len(verts) - sides, len(verts)))]) return mesh_object(part, verts, faces, role) def loft(part, rows, role, segments=28, start=0, span=math.tau): """Tailored cross sections: (height, half width, half depth, center x, center z).""" verts, faces = [], [] closed = abs(span - math.tau) < 0.001 count = segments if closed else segments + 1 for y, rx, rz, cx, cz in rows: for j in range(count): angle = start + span * j / segments verts.append((cx + rx * math.cos(angle), y, cz + rz * math.sin(angle))) for row in range(len(rows) - 1): for j in range(segments): a, b = row * count + j, row * count + (j + 1) % count faces.append((a, a + count, b + count, b)) if closed: faces.extend([tuple(range(count)), tuple(reversed(range(len(verts) - count, len(verts))))]) return mesh_object(part, verts, faces, role) def finish_part(name, pivot, stout=False): pieces = PIECES.pop(name) bpy.ops.object.select_all(action="DESELECT") for obj in pieces: obj.select_set(True) bpy.context.view_layer.objects.active = pieces[0] bpy.ops.object.join() obj = bpy.context.object obj.name = name bpy.ops.object.transform_apply(location=False, rotation=True, scale=True) bpy.context.scene.cursor.location = coord(pivot) bpy.ops.object.origin_set(type="ORIGIN_CURSOR") obj.data.materials.clear() obj.data.materials.append(bpy.data.materials["TravelerPalette"]) for face in obj.data.polygons: face.material_index = 0 if stout: obj.shape_key_add(name="Basis") key = obj.shape_key_add(name="Stout") for point in key.data: world_y = point.co.z + pivot[1] belly = math.exp(-((world_y - 0.97) / 0.42) ** 2) world_x = point.co.x + pivot[0] world_z = -point.co.y + pivot[2] point.co.x += world_x * (0.13 + 0.32 * belly) point.co.y -= world_z * (0.14 + 0.42 * belly) key.value = 0.0 obj.data.calc_loop_triangles() LIBRARY[name] = obj PIVOTS[name] = pivot return obj def torso(style): part = "Torso" + style rows = [(0.66, .32, .24, 0, 0), (.75, .39, .29, 0, 0), (.90, .405, .31, 0, .01), (1.10, .385, .30, 0, .01), (1.30, .325, .26, 0, 0), (1.43, .27, .20, 0, 0), (1.51, .18, .15, 0, 0)] loft(part, rows, "linen") loft(part, [(1.44, .17, .14, 0, 0), (1.63, .18, .15, 0, 0), (1.73, .21, .16, 0, 0)], "fur", 24) for y, z in [(1.03, .325), (1.15, .310), (1.27, .279)]: ellipsoid(part, (0, y, z), (.014, .014, .011), "leather", 10, 6) tube(part, [(-.043, .94, .327), (-.043, 1.14, .309), (-.043, 1.36, .235)], .004, "muzzle", 5) outer = [(y, rx + .025, rz + .022, cx, cz) for y, rx, rz, cx, cz in rows] if style == "Coat": outer = [(0.45, .355, .26, 0, -.01), (.57, .415, .30, 0, -.01)] + outer if style == "Tunic": loft(part, [(y, rx + .018, rz + .018, cx, cz) for y, rx, rz, cx, cz in rows[:-1]], "coat") rounded_box(part, (0, .91, .365), (.40, .42, .028), .025, "linen") tube(part, [(-.19, 1.13, .315), (0, 1.15, .338), (.19, 1.13, .315)], .012, "coat_shadow", 6) else: gap = .31 loft(part, outer, "coat", start=math.pi / 2 + gap, span=math.tau - 2 * gap) for sign in (-1, 1): edge = [(sign * rx * math.sin(gap), y, cz + rz * math.cos(gap)) for y, rx, rz, cx, cz in outer] tube(part, edge, .012, "coat_shadow", 6) rounded_box(part, (sign * .275, .835, .272), (.19, .17, .039), .027, "coat", tilt=sign * 17) tube(part, [(sign * .18, .91, .315), (sign * .27, .914, .308), (sign * .36, .907, .259)], .006, "coat_shadow", 5) for y, z in [(.79, .310), (.97, .331), (1.16, .309)]: ellipsoid(part, (.15, y, z), (.025, .026, .013), "ivory", 12, 8) ellipsoid(part, (.15, y, z + .012), (.009, .008, .004), "coat_shadow", 8, 4) # A stitched waistband and buckle sit over the trouser waist. loft(part, [(.68, .343, .252, 0, .0), (.727, .369, .277, 0, .0)], "leather") rounded_box(part, (0, .703, .291), (.115, .060, .022), .010, "brass") rounded_box(part, (0, .703, .307), (.076, .035, .008), .004, "leather") for sign in (-1, 1): mesh_object(part, [(sign * .05, 1.47, .17), (sign * .20, 1.46, .17), (sign * .24, 1.32, .235), (sign * .11, 1.38, .25)], [(0, 1, 2, 3)] if sign > 0 else [(3, 2, 1, 0)], "linen") finish_part(part, (0, 1.05, 0), True) def arm(side, bent): sign = -1 if side == "Left" else 1 x = sign * .44 name = "Arm" + ("Hiker" if bent else "Relaxed") + side if bent: points = [(x, 1.35, 0), (x + sign * .12, 1.18, .07), (x + sign * .10, 1.05, .20), (x - sign * .035, 1.14, .32)] hand = (x - sign * .055, 1.20, .33) tube(name, points, [.135, .17, .165, .12], "linen", 18) ellipsoid(name, points[0], (.14, .145, .14), "linen", 20, 12) ellipsoid(name, points[2], (.165, .14, .15), "linen") ellipsoid(name, points[-1], (.135, .082, .13), "muzzle") else: loft(name, [(y, rx, rz, x + sign * offset, z) for y, rx, rz, offset, z in [ (.78, .12, .13, .025, .06), (.88, .135, .15, .035, .03), (1.03, .17, .16, .045, .005), (1.21, .16, .145, .025, 0), (1.35, .12, .12, 0, 0), (1.41, .055, .07, -.025, 0)]], "linen", 20) loft(name, [(.78, .133, .141, x + sign * .025, .06), (.84, .143, .15, x + sign * .028, .052)], "muzzle", 20) hand = (x + sign * .03, .694, .065) ellipsoid(name, hand, (.12, .12, .105), "fur") ellipsoid(name, (hand[0] - sign * .105, hand[1] + .023, hand[2] + .035), (.051, .066, .062), "fur", 14, 8) for index in (-1, 0, 1): tube(name, [(hand[0] + index * .042, hand[1] - .041, hand[2] + .096), (hand[0] + index * .042, hand[1] - .070, hand[2] + .08)], .004, "fur_shadow", 5) finish_part(name, (x, 1.30, 0)) def leg(side): x = -.20 if side == "Left" else .20 name = "Leg" + side loft(name, [(.20, .115, .13, x, .01), (.32, .157, .15, x, 0), (.43, .17, .16, x, -.01), (.59, .18, .17, x, -.01), (.75, .18, .165, x, 0)], "trousers", 20) tube(name, [(x + .15, .35, .06), (x + .16, .47, .07), (x + .15, .66, .07)], .005, "coat_shadow", 5) rounded_box(name, (x, .035, .095), (.335, .056, .57), .025, "ink") ellipsoid(name, (x, .129, .12), (.167, .105, .284), "leather") loft(name, [(.13, .142, .15, x, -.01), (.24, .137, .136, x, -.03), (.33, .155, .143, x, -.03)], "leather", 20) loft(name, [(.294, .163, .151, x, -.03), (.34, .161, .148, x, -.03)], "fur_shadow", 20) tube(name, [(x - .075, .21, .24), (x, .228, .26), (x + .075, .21, .24)], .008, "brass", 6) finish_part(name, (x, .61, 0)) def head(species): name = "Head" + species width = {"Cat": .49, "Fox": .45, "Rabbit": .445, "Badger": .51, "Otter": .485}[species] depth = .34 verts, faces = [], [] rings, segments = 20, 40 for row in range(rings + 1): phi = math.pi * row / rings vertical = math.cos(phi) cheek = 1.0 + .14 * math.exp(-((vertical + .22) / .49) ** 2) for j in range(segments): angle = math.tau * j / segments verts.append((width * math.sin(phi) * math.cos(angle) * cheek, 1.93 + .435 * vertical, depth * math.sin(phi) * math.sin(angle))) for row in range(rings): for j in range(segments): a, b = row * segments + j, row * segments + (j + 1) % segments faces.append((b, b + segments, a + segments, a)) obj = mesh_object(name, verts, faces, "fur") colors = obj.data.color_attributes["Color"] uv = obj.data.uv_layers["PaletteRole"] for polygon in obj.data.polygons: center = sum((Vector(verts[i]) for i in polygon.vertices), Vector()) / len(polygon.vertices) role = "fur" if center.z > .03 and center.y < 1.985 - .18 * math.exp(-(center.x / .15) ** 2): role = "muzzle" if species == "Badger" and center.z > .08 and .105 < abs(center.x) < .32 and center.y > 1.80: role = "fur_shadow" for index in polygon.loop_indices: colors.data[index].color = (*linear(PALETTE[role][1]), 1) uv.data[index].uv = (PALETTE[role][0], 0) muzzle_depth = .20 if species == "Fox" else .11 muzzle_z = .335 if species == "Fox" else .315 for sign in (-1, 1): ellipsoid(name, (sign * .105, 1.843, muzzle_z), (.15, .105, muzzle_depth), "muzzle", 20, 12) nose_z = muzzle_z + muzzle_depth * .91 rounded_box(name, (0, 1.927, nose_z), (.096, .054, .055), .016, "fur_shadow" if species != "Rabbit" else "pink") tube(name, [(0, 1.90, nose_z + .024), (0, 1.854, nose_z + .007)], .007, "ink", 6) for sign in (-1, 1): tube(name, [(0, 1.854, nose_z + .007), (sign * .065, 1.825, nose_z - .005), (sign * .135, 1.839, nose_z - .040), (sign * .17, 1.877, nose_z - .064)], [.006, .007, .006, .002], "ink", 6) eye_x = sign * .213 # Almond eyes with lids and a visible iris, seated into the face. ellipsoid(name, (eye_x, 2.077, .301), (.109, .055, .043), "fur_shadow", 24, 12) ellipsoid(name, (eye_x, 2.075, .322), (.098, .041, .030), "ivory", 24, 12) ellipsoid(name, (eye_x - sign * .011, 2.075, .350), (.035, .039, .011), "iris", 16, 10) ellipsoid(name, (eye_x - sign * .011, 2.076, .359), (.016, .031, .008), "ink", 12, 8) ellipsoid(name, (eye_x - sign * .022, 2.094, .365), (.009, .011, .005), "ivory", 8, 6) tube(name, [(eye_x - .101, 2.076, .318), (eye_x - .04, 2.105, .35), (eye_x + .035, 2.102, .355), (eye_x + .102, 2.073, .312)], [.005, .012, .012, .003], "fur_shadow", 7) tube(name, [(eye_x - .079, 2.226 - sign * .013, .27), (eye_x, 2.242, .288), (eye_x + .067, 2.213 + sign * .007, .259)], [.009, .020, .007], "fur_shadow", 8) for j in range(3): y = 1.86 + j * .033 tube(name, [(sign * .26, y, .285), (sign * .47, y + (j - 1) * .025, .28), (sign * (.64 - j * .015), y + (j - 1) * .055, .24)], [.005, .0035, .001], "fur_shadow", 5) for x, y in [(0.20, 1.92), (.265, 1.88), (.22, 1.84)]: ellipsoid(name, (sign * x, y, .339), (.007, .007, .005), "fur_shadow", 8, 4) if species in ("Cat", "Fox"): for sign in (-1, 0, 1): tube(name, [(sign * .115, 2.327, .10), (sign * .112, 2.296, .185), (sign * .09, 2.248, .248)], [.019, .024, .003], "fur_shadow", 7) # Small swept fur tufts keep the head silhouette organic. for sign in (-1, 1): tube(name, [(sign * .41, 1.88, .06), (sign * .53, 1.91, .04), (sign * .57, 1.94, .02)], [.075, .035, .001], "fur", 8) finish_part(name, (0, 1.68, 0)) def ears(style): name = "Ears" + style for sign in (-1, 1): if style in ("Cat", "Fox"): height = .38 if style == "Cat" else .46 x = sign * .34 tube(name, [(x, 2.205, -.02), (x + sign * .05, 2.38, -.025), (x + sign * .092, 2.205 + height, -.04)], [.165, .105, .004], "fur", 12, .43) mesh_object(name, [(x - sign * .104, 2.22, .082), (x + sign * .127, 2.245, .070), (x + sign * .081, 2.205 + height - .051, -.015)], [(0, 1, 2)] if sign > 0 else [(2, 1, 0)], "muzzle") tube(name, [(x, 2.25, .105), (x + sign * .029, 2.31, .075), (x + sign * .053, 2.365, .035)], [.028, .020, .002], "pink", 6) elif style in ("Rabbit", "Lop"): x = sign * .25 points = [(x, 2.23, -.025), (x + sign * .02, 2.45, -.04), (x + sign * .07, 2.73, -.09), (x + sign * .10, 2.87, -.12)] if style == "Lop": points = [(x, 2.23, -.025), (x + sign * .12, 2.49, -.04), (x + sign * .25, 2.51, -.03), (x + sign * .31, 2.24, .015)] tube(name, points, [.105, .123, .103, .008], "fur", 14) tube(name, [(p[0], p[1], p[2] + .085) for p in points[1:]], [.060, .055, .006], "pink", 10) else: x = sign * .39 radius = .145 if style == "Badger" else .12 ellipsoid(name, (x, 2.265, -.015), (radius, radius * 1.05, .09), "fur", 20, 12) ellipsoid(name, (x, 2.27, .066), (radius * .62, radius * .63, .02), "pink", 16, 10) finish_part(name, (0, 2.20, 0)) def tail(species): name = "Tail" + species if species == "Rabbit": ellipsoid(name, (0, .70, -.38), (.19, .19, .18), "muzzle") else: points = [(0, .80, -.22), (.17, .58, -.47), (.43, .62, -.62), (.66, .91, -.61), (.71, 1.21, -.48), (.63, 1.43, -.37)] radii = [.065, .09, .125, .14, .12, .002] if species == "Fox": radii = [.095, .17, .24, .25, .19, .002] if species == "Badger": points, radii = [(0, .8, -.2), (.08, .56, -.43), (.15, .52, -.63)], [.07, .14, .003] if species == "Otter": points, radii = [(0, .8, -.2), (.05, .48, -.51), (.29, .24, -.80), (.51, .23, -.83)], [.13, .14, .10, .003] # Subdivide the sweep with Catmull-Rom interpolation for a soft, curling silhouette. smooth_points, smooth_radii = [], [] for i in range(len(points) - 1): p0, p1 = Vector(points[max(i - 1, 0)]), Vector(points[i]) p2, p3 = Vector(points[i + 1]), Vector(points[min(i + 2, len(points) - 1)]) for step in range(4): t = step / 4 smooth_points.append(.5 * ((2 * p1) + (-p0 + p2) * t + (2*p0 - 5*p1 + 4*p2 - p3)*t*t + (-p0 + 3*p1 - 3*p2 + p3)*t*t*t)) smooth_radii.append(radii[i] * (1 - t) + radii[i + 1] * t) smooth_points.append(Vector(points[-1])) smooth_radii.append(radii[-1]) tube(name, smooth_points, smooth_radii, "fur", 14) if species == "Fox": tube(name, points[-2:], [radii[-2] * 1.01, .002], "muzzle", 14) finish_part(name, (0, .8, -.22)) def accessories(): name = "Scarf" tube(name, [(-.19, 1.48, -.1), (-.23, 1.43, .06), (-.15, 1.41, .19), (0, 1.395, .24), (.18, 1.43, .13), (.20, 1.48, -.10)], .072, "scarf", 12) rounded_box(name, (-.17, 1.31, .269), (.145, .21, .062), .032, "scarf", tilt=-12) mesh_object(name, [(-.22, 1.35, .30), (-.10, 1.32, .32), (-.11, .68, .35), (-.20, .74, .355), (-.25, .67, .347), (-.27, 1.09, .35)], [(0, 5, 4, 3, 2, 1)], "scarf") for x in (-.23, -.20, -.17, -.14): tube(name, [(x, .73, .35), (x - .01, .62, .36)], [.008, .003], "scarf", 5) finish_part(name, (0, 1.05, 0), True) name = "BagTravel" rounded_box(name, (0, 1.24, -.46), (.67, 1.04, .39), .12, "leather") rounded_box(name, (0, 1.63, -.58), (.72, .32, .25), .085, "fur_shadow") for sign in (-1, 1): tube(name, [(sign * .23, 1.71, -.52), (sign * .29, 1.54, -.18), (sign * .275, 1.36, .20), (sign * .32, 1.05, .292), (sign * .37, .81, .15), (sign * .29, .76, -.37)], .032, "leather", 8) rounded_box(name, (sign * .27, 1.16, .311), (.081, .10, .031), .014, "brass") rounded_box(name, (sign * .345, 1.10, -.44), (.145, .35, .255), .040, "leather") tube(name, [(sign * .21, .85, -.672), (sign * .21, 1.3, -.677), (sign * .21, 1.67, -.714)], .021, "fur_shadow", 7) tube(name, [(-.43, 1.88, -.50), (.43, 1.88, -.50)], .215, "scarf", 28) for sign in (-1, 1): points = [(sign * .434, 1.88 + .18 * (1 - t / 34) * math.sin(t * .42), -.50 + .18 * (1 - t / 34) * math.cos(t * .42)) for t in range(33)] tube(name, points, .008, "coat_shadow", 5) ring = [(sign * .27, 1.88 + .223 * math.sin(t * math.tau / 24), -.50 + .223 * math.cos(t * math.tau / 24)) for t in range(25)] tube(name, ring, .018, "leather", 6) finish_part(name, (0, 1.05, 0), True) name = "BagSatchel" rounded_box(name, (.46, .80, .045), (.28, .35, .25), .07, "leather") rounded_box(name, (.47, .90, .16), (.29, .19, .07), .025, "fur_shadow") rounded_box(name, (.48, .85, .206), (.052, .076, .013), .008, "brass") tube(name, [(-.27, 1.46, -.05), (-.23, 1.40, .20), (.03, 1.15, .33), (.30, .90, .30), (.46, .88, .03)], .027, "leather", 8) finish_part(name, (0, 1.05, 0), True) name = "HatBeret" ellipsoid(name, (.035, 2.32, -.02), (.44, .155, .325), "coat", 28, 14) loft(name, [(2.255, .385, .277, 0, -.02), (2.31, .392, .285, 0, -.02)], "coat_shadow") tube(name, [(0, 2.445, -.01), (.027, 2.48, -.012), (.051, 2.479, -.025)], .021, "coat_shadow") finish_part(name, (0, 1.9, 0)) name = "HatBrim" ellipsoid(name, (0, 2.285, -.025), (.60, .045, .415), "leather", 36, 10) loft(name, [(2.29, .39, .28, 0, -.025), (2.43, .365, .26, 0, -.035), (2.55, .28, .215, -.035, -.035), (2.59, .21, .17, -.04, -.035)], "leather") loft(name, [(2.31, .396, .285, 0, -.025), (2.367, .386, .277, 0, -.03)], "scarf") finish_part(name, (0, 1.9, 0)) name = "Spectacles" for sign in (-1, 1): points = [(sign * .213 + .127 * math.cos(t * math.tau / 28), 2.073 + .099 * math.sin(t * math.tau / 28), .375) for t in range(29)] tube(name, points, .008, "brass", 5) tube(name, [(sign * .335, 2.08, .375), (sign * .46, 2.10, .16)], .008, "leather", 6) tube(name, [(-.086, 2.081, .375), (0, 2.109, .402), (.086, 2.081, .375)], .009, "brass", 6) finish_part(name, (0, 1.68, 0)) def export_and_preview(): bpy.ops.object.select_all(action="DESELECT") for obj in LIBRARY.values(): obj.select_set(True) bpy.ops.export_scene.gltf(filepath=str(OUTPUT / "woodland_parts.glb"), export_format="GLB", use_selection=True, export_yup=True, export_animations=False, export_morph=True, export_morph_normal=True, export_vertex_color="ACTIVE", export_materials="EXPORT", export_cameras=False, export_extras=False) budget = {} for name, obj in LIBRARY.items(): budget[name] = {"triangles": len(obj.data.loop_triangles), "surfaces": 1, "pivot": PIVOTS[name], "stout_shape": obj.data.shape_keys is not None} (OUTPUT / "mesh_budget.json").write_text(json.dumps(budget, indent=2) + "\n") # The saved file opens on the reference traveler; the complete kit stays editable. preview = bpy.data.collections.new("01 Reference traveler - assembled example") bpy.context.scene.collection.children.link(preview) selected = ["TorsoVest", "HeadCat", "EarsCat", "ArmHikerLeft", "ArmHikerRight", "LegLeft", "LegRight", "TailCat", "Scarf", "BagTravel"] for name, source in LIBRARY.items(): source.hide_render = True source.hide_set(True) if name not in selected: continue copy = source.copy() copy.data = source.data.copy() copy.name = "Preview_" + name preview.objects.link(copy) copy.hide_render = False copy.hide_set(False) if copy.data.shape_keys: copy.data.shape_keys.key_blocks["Stout"].value = .68 if name.startswith("Arm"): copy.location.x += (-1 if "Left" in name else 1) * .068 copy.scale.x = 1.1 if name.startswith("Leg"): copy.location.x += (-1 if "Left" in name else 1) * .034 copy.scale.x = 1.14 scene = bpy.context.scene scene.world.color = (.32, .36, .40) scene.render.engine = "CYCLES" scene.cycles.samples = 32 for position, energy, size in [((3, 5, 4), 450, 5), ((-3, 3, 1), 220, 4), ((1, 4, -3), 500, 3)]: bpy.ops.object.light_add(type="AREA", location=coord(position)) light = bpy.context.object light.data.energy = energy light.data.shape = "DISK" light.data.size = size light.rotation_euler = (coord((0, 1.25, 0)) - light.location).to_track_quat('-Z', 'Y').to_euler() bpy.ops.object.camera_add(location=coord((3.2, 2.6, 5.8))) camera = bpy.context.object camera.rotation_euler = (coord((0, 1.3, 0)) - camera.location).to_track_quat('-Z', 'Y').to_euler() camera.data.type = "ORTHO" camera.data.ortho_scale = 3.6 scene.camera = camera scene.render.resolution_x = 1000 scene.render.resolution_y = 1100 scene.render.resolution_percentage = 100 for screen in bpy.data.screens: for area in screen.areas: if area.type == "VIEW_3D": area.spaces.active.shading.type = "MATERIAL" area.spaces.active.region_3d.view_distance = 4.0 area.spaces.active.region_3d.view_location = coord((0, 1.3, 0)) bpy.context.preferences.filepaths.save_version = 0 bpy.ops.wm.save_as_mainfile(filepath=str(SOURCE / "woodland_character_kit.blend"), compress=True) print("CHARACTER KIT", len(LIBRARY), "parts", sum(v["triangles"] for v in budget.values()), "library triangles") bpy.ops.object.select_all(action="SELECT") bpy.ops.object.delete(use_global=False) material = bpy.data.materials.new("TravelerPalette") material.use_nodes = True tree = material.node_tree shader = tree.nodes.get("Principled BSDF") shader.inputs["Roughness"].default_value = .9 vertex = tree.nodes.new("ShaderNodeVertexColor") vertex.layer_name = "Color" tree.links.new(vertex.outputs["Color"], shader.inputs["Base Color"]) for style in ("Vest", "Coat", "Tunic"): torso(style) for side in ("Left", "Right"): arm(side, False) arm(side, True) leg(side) for species in ("Cat", "Fox", "Rabbit", "Badger", "Otter"): head(species) ears(species) tail(species) ears("Lop") accessories() export_and_preview()