122 lines
5.6 KiB
Python
122 lines
5.6 KiB
Python
"""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']))
|