# Bedding: an analytic pillow and a cloth-simulated blanket draped over the mattress and the bunk's sill. # blender -b --factory-startup --python sim_bedding.py # Needs ../CaptainsQuarters.blend from a build (its bunk wall, interior and mattress are the colliders). # Writes cache/SM_CQ_Pillow.json and cache/SM_CQ_Blanket.json, which build_quarters.py loads. import bpy, bmesh, os, sys, math, json, random import numpy as np from mathutils import Vector HERE = os.path.dirname(os.path.abspath(__file__)) ROOT = os.path.dirname(HERE) CACHE = os.path.join(HERE, 'cache') os.makedirs(CACHE, exist_ok=True) SRC = os.path.join(ROOT, 'CaptainsQuarters.blend') MZ = 0.62 # mattress top FRAMES = 110 def dump(name, verts, faces, uvs): json.dump({'verts': [[round(c, 5) for c in v] for v in verts], 'faces': faces, 'uvs': [[round(c, 5) for c in t] for t in uvs]}, open(os.path.join(CACHE, name + '.json'), 'w')) def lumps(seed, n=7): r = random.Random(seed) w = [(r.uniform(1.5, 6.0), r.uniform(1.5, 6.0), r.uniform(0, 6.28), r.uniform(0, 6.28), r.uniform(0.4, 1.0)) for _ in range(n)] def f(a, b): return sum(k * math.sin(fa * a * math.pi + pa) * math.sin(fb * b * math.pi + pb) for fa, fb, pa, pb, k in w) / n return f def pillow(): L, W, TH, RIM = 0.46, 0.70, 0.135, 0.028 cx, cy, yaw = -2.635, 1.575, math.radians(5.0) N, M = 26, 38 lf = lumps(3) verts, uvs, top, bot = [], [], {}, {} def P(a, b, z): x = (a - 0.5) * L * (1 - 0.07 * math.sin(math.pi * b)) y = (b - 0.5) * W * (1 - 0.05 * math.sin(math.pi * a)) xr = x * math.cos(yaw) - y * math.sin(yaw); yr = x * math.sin(yaw) + y * math.cos(yaw) return (cx + xr, cy + yr, z) for j in range(M + 1): for i in range(N + 1): a, b = i / N, j / M s = (math.sin(math.pi * a) * math.sin(math.pi * b)) ** 0.42 if 0 < i < N and 0 < j < M else 0.0 dent = 0.030 * math.exp(-(((a - 0.5) / 0.30) ** 2 + ((b - 0.48) / 0.22) ** 2)) zt = MZ + RIM + (TH - RIM) * s * (1 + 0.10 * lf(a, b)) - dent * s top[(i, j)] = len(verts); verts.append(P(a, b, zt)); uvs.append((a * L, b * W)) if 0 < i < N and 0 < j < M: zb = MZ + 0.003 + RIM * (1 - s) ** 1.6 bot[(i, j)] = len(verts); verts.append(P(a, b, zb)); uvs.append((a * L + 0.5, b * W)) else: bot[(i, j)] = top[(i, j)] faces = [] for j in range(M): for i in range(N): faces.append([top[(i, j)], top[(i + 1, j)], top[(i + 1, j + 1)], top[(i, j + 1)]]) faces.append([bot[(i, j)], bot[(i, j + 1)], bot[(i + 1, j + 1)], bot[(i + 1, j)]]) dump('SM_CQ_Pillow', verts, faces, uvs) return verts, faces def mesh_obj(name, verts, faces): me = bpy.data.meshes.new(name); me.from_pydata(verts, [], faces); me.update() ob = bpy.data.objects.new(name, me); bpy.context.scene.collection.objects.link(ob) return ob def main(): bpy.ops.wm.read_factory_settings(use_empty=True) sc = bpy.context.scene pv, pf = pillow() with bpy.data.libraries.load(SRC, link=False) as (src, dst): dst.objects = [n for n in src.objects if n in ('SM_CQ_Wall_Bunk', 'SM_CQ_Bunk_Interior', 'SM_CQ_Mattress', 'SM_CQ_Floor')] cols = list(dst.objects) for ob in cols: sc.collection.objects.link(ob) cols.append(mesh_obj('PillowCol', pv, pf)) for ob in cols: md = ob.modifiers.new('Collision', 'COLLISION') ob.collision.thickness_outer = 0.006 ob.collision.thickness_inner = 0.01 ob.collision.cloth_friction = 45.0 ob.collision.damping = 0.9 # blanket sheet: from the back of the mattress to 0.42 m beyond the sill, slightly wavy x0, x1, y0, y1 = -2.44, -0.80, 0.80, 2.00 cell = 0.0165 nx = int(round((x1 - x0) / cell)); ny = int(round((y1 - y0) / cell)) lf = lumps(11, 9) verts, faces, uvs = [], [], [] for j in range(ny + 1): for i in range(nx + 1): a, b = i / nx, j / ny verts.append((x0 + (x1 - x0) * a, y0 + (y1 - y0) * b, MZ + 0.085 + 0.055 * lf(a, b))) uvs.append(((x1 - x0) * a, (y1 - y0) * b)) for j in range(ny): for i in range(nx): k = j * (nx + 1) + i faces.append([k, k + 1, k + nx + 2, k + nx + 1]) ob = mesh_obj('Blanket', verts, faces) vg = ob.vertex_groups.new(name='pin') vg.add([j * (nx + 1) + i for j in (ny,) for i in range(nx + 1)], 1.0, 'REPLACE') md = ob.modifiers.new('Cloth', 'CLOTH') cs = md.settings cs.quality = 10 cs.mass = 0.22 cs.air_damping = 1.5 cs.tension_stiffness = 10.0; cs.compression_stiffness = 10.0 cs.shear_stiffness = 3.0; cs.bending_stiffness = 0.18 cs.tension_damping = 5.0; cs.compression_damping = 5.0; cs.shear_damping = 5.0; cs.bending_damping = 0.5 cs.vertex_group_mass = 'pin' cs.shrink_min = -0.24 # the sheet is 14 % larger than its outline: it buckles into folds cc = md.collision_settings cc.use_collision = True; cc.collision_quality = 5; cc.distance_min = 0.007 cc.use_self_collision = True; cc.self_distance_min = 0.006; cc.self_friction = 10.0 md.point_cache.frame_start = 1; md.point_cache.frame_end = FRAMES sc.frame_start = 1; sc.frame_end = FRAMES for f in range(1, FRAMES + 1): sc.frame_set(f) if f % 20 == 0: print('frame', f); sys.stdout.flush() dg = bpy.context.evaluated_depsgraph_get() ev = ob.evaluated_get(dg) co = np.empty(len(ev.data.vertices) * 3, dtype=np.float64) ev.data.vertices.foreach_get('co', co) co = co.reshape(-1, 3) # a light relaxation takes the solver's jitter out without flattening the folds bm = bmesh.new() for v in co: bm.verts.new(v) bm.verts.ensure_lookup_table() for f in faces: bm.faces.new([bm.verts[i] for i in f]) for _ in range(2): bmesh.ops.smooth_vert(bm, verts=bm.verts, factor=0.5, use_axis_x=True, use_axis_y=True, use_axis_z=True) out = [tuple(v.co) for v in bm.verts] bm.free() zs = [v[2] for v in out]; ys = [v[1] for v in out] dump('SM_CQ_Blanket', out, faces, uvs) print('BEDDING OK', len(out), 'z', round(min(zs), 3), round(max(zs), 3), 'y', round(min(ys), 3), round(max(ys), 3)) if __name__ == '__main__': main()