"""Run through Blender MCP. Separate scene, four colors, articulated rigid parts.""" import bpy, math, os from mathutils import Vector ROOT = 'D:/project/sunrise' scene = bpy.data.scenes.new('Red Sheep Character') bpy.context.window.scene = scene palette = {'Red': 'D94B3D', 'Cream': 'FFF0D6', 'Ink': '352724', 'Gold': 'EAAF45'} mats = {} for name, color in palette.items(): m = bpy.data.materials.new('Sheep_' + name) m.use_nodes = True rgb = [int(color[i:i+2], 16) / 255 for i in (0, 2, 4)] linear = [v / 12.92 if v < .04045 else ((v + .055) / 1.055) ** 2.4 for v in rgb] m.diffuse_color = (*linear, 1) node = next(n for n in m.node_tree.nodes if n.type == 'BSDF_PRINCIPLED') node.inputs['Base Color'].default_value = (*linear, 1) node.inputs['Roughness'].default_value = 1 mats[name] = m def empty(name, loc, parent=None): obj = bpy.data.objects.new(name, None) scene.collection.objects.link(obj) obj.parent = parent obj.location = loc return obj root = empty('RedSheep', (0, 0, 0)) parts = {} def ball(name, loc, scale, color, parent=root): bpy.ops.mesh.primitive_uv_sphere_add(segments=20, ring_count=12, location=loc) obj = bpy.context.object obj.name = name obj.scale = scale bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) obj.data.materials.append(mats[color]) for p in obj.data.polygons: p.use_smooth = True obj.parent = parent parts.setdefault(parent, []).append(obj) return obj def stroke(name, coords, radius, color, parent=root): curve = bpy.data.curves.new(name, 'CURVE') curve.dimensions = '3D' curve.bevel_depth = radius curve.bevel_resolution = 2 spline = curve.splines.new('POLY') spline.points.add(len(coords)-1) for point, xyz in zip(spline.points, coords): point.co = (*xyz, 1) obj = bpy.data.objects.new(name, curve) scene.collection.objects.link(obj) obj.data.materials.append(mats[color]) bpy.context.view_layer.objects.active = obj obj.select_set(True) bpy.ops.object.convert(target='MESH') obj = bpy.context.object obj.parent = parent parts.setdefault(parent, []).append(obj) obj.select_set(False) return obj body = empty('Body', (0, 0, 0), root) ball('Wool body', (0, 0, 1.25), (.55, .37, .58), 'Red', body) for x in (-.32, .32): ball('Hip wool', (x, 0, .96), (.28, .32, .29), 'Red', body) ball('Tail', (0, .42, 1.02), (.22, .19, .22), 'Red', body) for side, x in [('Left', -.27), ('Right', .27)]: leg = empty('Leg' + side, (x, 0, .85), root) ball('Cream leg', (0, 0, -.35), (.18, .18, .36), 'Cream', leg) ball('Hoof', (0, -.055, -.70), (.21, .24, .15), 'Ink', leg) arm = empty('Arm' + side, (x / .27 * .52, 0, 1.65), root) ball('Sleeve', (x / .27 * .065, 0, -.18), (.21, .22, .27), 'Red', arm) ball('Forearm', (x / .27 * .12, -.02, -.40), (.145, .15, .23), 'Cream', arm) ball('Hand', (x / .27 * .14, -.04, -.58), (.17, .17, .17), 'Ink', arm) head = empty('Head', (0, 0, 2.38), root) ball('Head wool', (0, 0, 0), (.66, .43, .56), 'Red', head) ball('Cream face', (0, -.315, -.05), (.47, .20, .39), 'Cream', head) for x,z,s in [(-.44,.28,.25),(-.23,.46,.26),(0,.50,.27),(.25,.44,.25),(.46,.25,.24),(-.52,-.22,.19),(.52,-.22,.19)]: ball('Wool tuft', (x, -.10, z), (s, .33, s), 'Red', head) for sign in (-1, 1): ear = ball('Ear', (sign*.70, -.035, -.12), (.30, .12, .15), 'Cream', head) ear.rotation_euler.y = sign * .35 ball('Horn', (sign*.64, -.06, .25), (.24, .25, .27), 'Ink', head) spiral = [] for i in range(32): t = i / 31 * math.pi * 1.7 r = .16 - i / 31 * .11 spiral.append((sign*(.64 + math.cos(t)*r), -.305, .25+math.sin(t)*r)) stroke('Horn curl', spiral, .022, 'Cream', head) ball('Eye', (sign*.18, -.503, -.035), (.048, .025, .065), 'Ink', head) ball('Nose', (0, -.522, -.16), (.048, .025, .03), 'Ink', head) stroke('Mouth', [(-.11,-.51,-.21),(-.075,-.527,-.24),(-.035,-.535,-.235),(0,-.537,-.20),(.035,-.535,-.235),(.075,-.527,-.24),(.11,-.51,-.21)], .015, 'Ink', head) # Small flat sunrise badge, facing -Y. verts = [(0, -.374, 1.43)] + [(.13*math.cos(i*math.pi/16), -.374, 1.43+.13*math.sin(i*math.pi/16)) for i in range(17)] mesh = bpy.data.meshes.new('Sun badge') mesh.from_pydata(verts, [], [(0,i+1,i+2) for i in range(16)]) badge = bpy.data.objects.new('Sun badge', mesh) scene.collection.objects.link(badge) badge.data.materials.append(mats['Gold']) badge.parent = body parts[body].append(badge) for a in (0, math.pi/4, math.pi/2, 3*math.pi/4, math.pi): stroke('Sun ray', [(r*math.cos(a), -.378, 1.43+r*math.sin(a)) for r in (.19,.25)], .018, 'Gold', body) # Merge static pieces per joint so the runtime only needs six meshes plus outlines. for pivot, objects in parts.items(): bpy.ops.object.select_all(action='DESELECT') for obj in objects: obj.select_set(True) bpy.context.view_layer.objects.active = objects[0] bpy.ops.object.join() obj = bpy.context.object obj.name = pivot.name + 'Mesh' # Joining preserves each mesh in the pivot's local coordinate system. bpy.ops.object.select_all(action='DESELECT') for obj in scene.objects: obj.select_set(True) bpy.context.view_layer.objects.active = root os.makedirs(ROOT + '/public/characters', exist_ok=True) os.makedirs(ROOT + '/assets/characters', exist_ok=True) bpy.ops.export_scene.gltf(filepath=ROOT+'/public/characters/red-sheep.glb', use_selection=True, use_active_scene=True, export_format='GLB', export_yup=True) bpy.ops.wm.save_as_mainfile(filepath=ROOT+'/assets/characters/red-sheep.blend', copy=True) print('Exported red-sheep.glb; meshes:', len([o for o in scene.objects if o.type == 'MESH']))