diff --git a/tools/sim_robots/hakobu/README.md b/tools/sim_robots/hakobu/README.md index 21f441d..fd0c2f2 100644 --- a/tools/sim_robots/hakobu/README.md +++ b/tools/sim_robots/hakobu/README.md @@ -5,21 +5,21 @@ 同じ場面に出る人(粘土のような簡略化した人)に合わせて、体の角を丸めた形をもとにする(2026-11-13、エイイチの決定)。頭(画面のまわり)は三面図どおりで、画面の面は斜めにスパッと切れたまま。 - 元にした絵:iCloud の ハコブくん/4コママンガ作成データ/キャラ/hakobu_front.png・hakobu_side.png・hakobu_back.png -- 大きさ:三面図のハコブくんの実寸(幅0.653・奥行0.760・高さ1.150m。幅は台の一番広い所。横の厚い板の所だけ幅0.674m)。エイイチの指示(2026-11-13)。病院の試算のロボット(hospital-unity-params.json の robot.size_m=KEENON W3 の幅0.459・奥行0.549・高さ1.081m)より大きい +- 大きさ:三面図のハコブくんの実寸(幅0.653・奥行0.760・高さ1.150m。幅は台の一番広い所。横の面は真ん中だけ外へ1cmふくらむので、そこの幅は0.673m)。エイイチの指示(2026-11-13)。病院の試算のロボット(hospital-unity-params.json の robot.size_m=KEENON W3 の幅0.459・奥行0.549・高さ1.081m)より大きい - 向き:根(Hakobu)は床の中心。顔が Unity の +Z(進む向き)を向く ## 作り方 1. `python3 hakobu_profiles.py <キャラのフォルダ> hakobu_profiles.json`:三面図から外形を測る(済んでいる。三面図が変わった時だけ) -2. `pip install bpy==5.3.0`(Blender を Python から使う)→ `python3 hakobu_build.py --out <出力フォルダ>`:hakobu.fbx・hakobu_albedo.png・hakobu_emission.png・hakobu_report.json を作る。大きさの既定は実寸。`--W --D --H`(m)で変えられる。丸めの既定は `--soft 3.5`(体の角を三面図の3.5倍の半径に。`--soft 1` で三面図どおりの角)。横の厚い板は既定で付く(`--no-pods` で外す) +2. `pip install bpy==5.3.0`(Blender を Python から使う)→ `python3 hakobu_build.py --out <出力フォルダ>`:hakobu.fbx・hakobu_albedo.png・hakobu_emission.png・hakobu_report.json を作る。大きさの既定は実寸。`--W --D --H`(m)で変えられる。丸めの既定は `--soft 3.5`(体の角を三面図の3.5倍の半径に。`--soft 1` で三面図どおりの角)。横の面のふくらみは `--crown 0.010`(m・0 で平ら)。横に厚い板を貼る `--pods` は使わない(2026-11-13 にやめた) 3. `python3 hakobu_render.py <出力>/hakobu.blend <絵のフォルダ> front side back persp sim sim_open`:確認用の絵(GitHub に入れない。iCloud の サービスロボット導入シミュレーション/hospital/hakobu_3d/) -## 中身(三角形 5,804・材質1つ) +## 中身(三角形 6,792・材質1つ) | 名前 | 中身 | 三角形 | |---|---|---| | Hakobu | 根(空の物)。床の中心 | – | -| Hakobu_Body | 胴・頭・台・車輪・バンパー・横の厚い板(両側) | 4,760 | +| Hakobu_Body | 胴・頭・台・車輪・バンパー・画面のまわりの丸いふち | 5,748 | | Hakobu_Face | 画面(顔) | 60 | | Hakobu_Ring | 横の輪(両側) | 432 | | Hakobu_Drawer | 引き出し(前の板・取っ手・中の箱)。前へ 0.26m まで引き出せる | 312 | @@ -27,7 +27,10 @@ - 色は1枚の画像(hakobu_albedo.png・1024px)にまとめた。光る所(目・口・輪・背中の帯)は hakobu_emission.png。材質は Hakobu_Mat の1つだけ - 丸めたのは体だけ:胴の縦の角(高さの6.4割から下。7割までで三面図の角に戻す)、台と黒い帯の角、引き出しの前の板とトレー、タイヤ(太くして角を丸め、上半分を台の中に隠した) -- 横の厚い板:三面図の横の板(輪の付いた所)の輪郭どおりに、厚さ約2cm・ふちの丸み2cmの板を両側に足した(マンガ ep147 のハコブくんの、お腹の横のふくらみ)。輪はこの板の表面に乗る +- 横:胴の横の面そのものを、真ん中ほど外へゆるくふくらませた(最大1cm・角は動かさない・高さ0.15〜0.66 で付け、頭と台の近くで0に戻す)。前は厚さ2cmの板を横に貼っていたが、横がパネルに見えたのでやめた(2026-11-13 エイイチの指摘)。輪はふくらんだ面の、輪の中心の所に乗る。輪の外形(三面図の横の板の輪郭)は継ぎ目の線として画像で描く +- 横の下:車輪の間のへこんだ灰色の板を画像で描いた(三面図の横) +- 画面のまわり:薄い灰色の丸いふち(画面より2cm大きい・角の丸み4.2cm)を画面の後ろに置いた(三面図の、画面を囲む枠) +- 車輪:半径0.046m・幅0.052m の太いタイヤを、上半分を台の中に隠す。台の底の角の丸みは1.2cm(大きく丸めると台の底が細り、車輪が脚のように見えた。小さい車輪にしても同じに見えた。2026-11-13) - 横の継ぎ目・頭の帯・背中の扉と操作箱・前の引き出しのすき間は、形ではなく画像で描いている(三角形を増やさないため) - Tray_1〜4 は FBX では根の直下にある。引き出しの子にすると、FBX の向きの変換が二重にかかってトレーが90°回る(Blender で読み直して確かめた)。Unity のプレハブで Drawer の子にする - 輪と顔を別の部品にしてあるのは、あとで状態(移動中・止まった・完了)で色や表情を変えられるようにするため diff --git a/tools/sim_robots/hakobu/hakobu_build.py b/tools/sim_robots/hakobu/hakobu_build.py index 50478be..2bdafa8 100644 --- a/tools/sim_robots/hakobu/hakobu_build.py +++ b/tools/sim_robots/hakobu/hakobu_build.py @@ -32,6 +32,7 @@ COL = { "screen": (14, 14, 16), "green": (96, 236, 140), "drawer_in": (196, 195, 190), + "rim": (184, 186, 186), # 画面のまわりの丸いふち "tray": (206, 228, 240), "eye": (250, 250, 250), "mouth": (84, 170, 236), @@ -45,7 +46,7 @@ R_FRONT = (768, 0, 256, 512) R_BACK = (0, 512, 256, 512) SW_ORIGIN = (256, 512) SW_CELL = 64 -SWATCH_NAMES = ["white", "band", "base", "black", "screen", "green", "drawer_in", "tray", "door"] +SWATCH_NAMES = ["white", "band", "base", "black", "screen", "green", "drawer_in", "tray", "door", "rim"] def swatch_px(name): @@ -223,6 +224,26 @@ def shell_levels(prof, W, D, H, t0=0.105, t1=0.996, tol=0.0022): BELLY = 1.0 # 胴の角だけに掛ける追加の倍率(--belly) PODS = False # 横の丸いふくらみ(--pods) SOFT = 1.0 # 角の丸さの倍率(人の粘土の見た目に合わせるときは --soft で大きくする) +CROWN = 0.010 # 横の面のふくらみ(m)。角は動かさず、横の面の真ん中ほど外へ出す(--crown。0 で平ら) + + +def crown_w(z, H): + """横のふくらみを付ける高さの重み:胴(0.15〜0.66)で1、頭(0.70 から上)と台の近くで0""" + t = z / H + if t < 0.11 or t > 0.70: + return 0.0 + if t < 0.15: + return (t - 0.11) / 0.04 + if t > 0.66: + return (0.70 - t) / 0.04 + return 1.0 + + +def crown_shift(y, z, df, db, H, rf, rb): + """横の面が外へ出る量。角は動かさず、横の直線の真ん中ほど外へ出す(4f(1-f))""" + y0, y1 = df + rf, db - rb + f = min(max((y - y0) / max(y1 - y0, 1e-6), 0.0), 1.0) + return CROWN * crown_w(z, H) * 4 * f * (1 - f) def corner_r(z, H, a, depth, front): @@ -277,6 +298,9 @@ def build_shell(bm, levels, W, D, H, seg=3): rf = corner_r(z, H, a, db - df, True) rb = corner_r(z, H, a, db - df, False) pts = rrect(-a, a, df, db, {"br": (rf, rf), "tr": (rb, rb), "tl": (rb, rb), "bl": (rf, rf)}, seg) + if CROWN > 0: # 横の面をふくらませる:横の面の点を、前後へ行くほど内へ寄せる(前と後ろの面はそのまま) + w = crown_w(z, H) + pts = [(x + (1 if x > 0 else -1) * CROWN * w * 4 * f * (1 - f), y) for x, y, f in _densify_sides(pts, seg)] rings.append([bm.verts.new((x, y, z)) for x, y in pts]) faces = [] n = len(rings[0]) @@ -290,6 +314,21 @@ def build_shell(bm, levels, W, D, H, seg=3): return faces +def _densify_sides(pts, seg, n=10): + """横の面の直線(右:br の終わり〜tr の始め、左:tl の終わり〜bl の始め)に n 点ずつ足す。 + ふくらみを曲線で見せるため。どの段でも点の数は同じ""" + k = seg + 1 + out = [] # (x, y, f):f は横の直線の上の位置(0〜1)。角の点は 0(動かさない) + for i, p in enumerate(pts): + out.append((p[0], p[1], 0.0)) + if i in (k - 1, 3 * k - 1): + q = pts[i + 1] + for j in range(1, n + 1): + f = j / (n + 1) + out.append((p[0] + (q[0] - p[0]) * f, p[1] + (q[1] - p[1]) * f, f)) + return out + + def uv_shell(bm, faces, W, D, H): lay = bm.loops.layers.uv.verify() bm.normal_update() @@ -322,7 +361,8 @@ def build_base(bm, W, D, H): bm.normal_update() top = cap_edges(faces, "z", z1) bot = cap_edges(faces, "z", z0) - bmesh.ops.bevel(bm, geom=bot, offset=0.016 * SOFT, segments=2 + (1 if SOFT > 1 else 0), profile=0.5, affect="EDGES", clamp_overlap=True) + # 底の角の丸みは小さく(大きく丸めると台の底が細って、車輪が台から離れた脚のように見えた・2026-11-13) + bmesh.ops.bevel(bm, geom=bot, offset=0.012, segments=2 + (1 if SOFT > 1 else 0), profile=0.5, affect="EDGES", clamp_overlap=True) bm.normal_update() top = [e for e in bm.edges if e.is_valid and all(abs(v.co.z - z1) < 1e-6 for v in e.verts) and len(e.link_faces) == 2 and any(abs(f.normal.z) < 0.9 for f in e.link_faces)] @@ -332,6 +372,7 @@ def build_base(bm, W, D, H): def build_wheels(bm, W, D, H, r=0.036, width=0.036, seg=12): if SOFT > 1: # 丸い見た目のとき:少し大きく太いタイヤを、上半分を台の中に隠して置き、角を丸める + # (小さくすると台の下から脚のように出て見えた。2026-11-13 エイイチの指摘でこの形に戻した) r, width, seg = 0.046, 0.052, 20 faces = [] for sx in (-1, 1): @@ -386,7 +427,7 @@ def build_face_plate(bm, prof, W, D, H): L = (b - a).length pts = rrect(-hw, hw, 0, L, {k: (0.016, 0.016) for k in ("br", "tr", "tl", "bl")}, 3) # 画面は丸めの対象にしない thick = 0.004 - base = a + nrm * 0.0055 + base = a + nrm * 0.0075 # まわりの丸いふち(0.0045 まで)より前に出す def P(u, v, s): return base + Vector((u, 0, 0)) + up * v + nrm * s @@ -419,6 +460,29 @@ def build_face_plate(bm, prof, W, D, H): return faces, (a, b, nrm, hw, L) +def build_bezel(bm, face_info, margin=0.020, r=0.042, s0=0.0015, s1=0.0045): + """画面のまわりの薄い灰色の丸いふち(三面図の、画面を囲む枠)。画面より少し大きく、画面の後ろに置く""" + a, b, nrm, hw, L = face_info + up = (b - a).normalized() + pts = rrect(-hw - margin, hw + margin, -margin, L + margin, {k: (r, r) for k in ("br", "tr", "tl", "bl")}, 6) + if signed_area(pts) < 0: + pts = pts[::-1] + + def P(u, v, sft): + return a + Vector((u, 0, 0)) + up * v + nrm * sft + back = [bm.verts.new(P(u, v, s0 - 0.004)) for u, v in pts] + front = [bm.verts.new(P(u, v, s1)) for u, v in pts] + faces = [bm.faces.new(front[::-1]), bm.faces.new(back)] + n = len(pts) + for i in range(n): + j = (i + 1) % n + faces.append(bm.faces.new((back[i], back[j], front[j], front[i]))) + bm.normal_update() + if faces[0].normal.dot(nrm) < 0: + faces[0].normal_flip() + return faces + + def build_ring(bm, W, D, H, x_side, r_out=0.122, r_in=0.1015, seg=36, proud=0.0006, depth=0.006): cy, cz = F["ring_c"][0] * D, F["ring_c"][1] * H faces, walls = [], [] @@ -607,7 +671,13 @@ def draw_atlas(W, D, H, face_info, ss=2): pts = rrect(pod["d"][0], pod["d"][1], pod["t"][0], pod["t"][1], {"tl": pod["r"]["ft"], "tr": pod["r"]["bt"], "br": pod["r"]["bb"], "bl": pod["r"]["fb"]}, 12) poly = [tuple(c * ss for c in spx(d, t)) for d, t in pts] - da.line(poly + [poly[0]], fill=(176, 174, 169), width=3 * ss, joint="curve") + da.line(poly + [poly[0]], fill=(158, 156, 152), width=4 * ss, joint="curve") + # 横の下:車輪の間のへこんだ灰色の板(三面図の横。上の角が丸く、下へ少し広がる) + sk = rrect(-0.27, 0.30, 0.0, 0.178, {"tl": (0.05, 0.05), "tr": (0.05, 0.05), "br": (0.0, 0.0), "bl": (0.0, 0.0)}, 10) + sk = [(d + (-0.02 if d < 0.015 else 0.02) * max(0.0, (0.14 - t) / 0.14), t) for d, t in sk] # 下へ少し広げる + poly = [tuple(c * ss for c in spx(d, t)) for d, t in sk] + da.polygon(poly, fill=(172, 171, 168)) + da.line(poly + [poly[0]], fill=(150, 149, 146), width=2 * ss, joint="curve") # 前の面 fx0, fy0, fw_, fh_ = R_FRONT @@ -753,8 +823,14 @@ def build(W, D, H, out_dir): set_uv_all(bm, bm.faces, swatch_uv("black")) ob_bump = new_object("Hakobu_Bumper_tmp", bm) + bm = bmesh.new() + build_bezel(bm, face_info) + finish(bm) + set_uv_all(bm, bm.faces, swatch_uv("rim")) + ob_bezel = new_object("Hakobu_Bezel_tmp", bm) + # まとめて1つの本体にする - body = join_objects("Hakobu_Body", [body_main, ob_base, ob_wheels, ob_bump]) + body = join_objects("Hakobu_Body", [body_main, ob_base, ob_wheels, ob_bump, ob_bezel]) body.parent = root body.data.materials.clear() body.data.materials.append(mat) @@ -764,6 +840,11 @@ def build(W, D, H, out_dir): # 輪: 横の面の外側(殻の横幅の一番広い所に合わせる) ring_lv = [l for l in levels if 0.38 * H <= l[0] <= 0.62 * H] x_side = max(l[1] for l in ring_lv) + cy_r, cz_r = F["ring_c"][0] * D, F["ring_c"][1] * H + lr = min(levels, key=lambda l: abs(l[0] - cz_r)) + rf_r = corner_r(lr[0], H, lr[1], lr[3] - lr[2], True) + rb_r = corner_r(lr[0], H, lr[1], lr[3] - lr[2], False) + x_side += crown_shift(cy_r, cz_r, lr[2], lr[3], H, rf_r, rb_r) # 輪の中心で、ふくらんだ殻の面に合わせる if PODS: # 横のふくらみを付けたときは、輪をふくらみの外の面に乗せる bm = bmesh.new() x_side = build_pods(bm, levels, W, D, H) @@ -776,7 +857,7 @@ def build(W, D, H, out_dir): body = body2 r_out = 0.5 * (313 / 2312 * H + 313 / 1529 * D) # 三面図の輪の外の半径(高さと奥行きの間をとる) bm = bmesh.new() - glow, walls = build_ring(bm, W, D, H, x_side, r_out=r_out, r_in=r_out * 261 / 313) + glow, walls = build_ring(bm, W, D, H, x_side, r_out=r_out, r_in=r_out * 261 / 313, proud=0.0018, depth=0.008) set_uv_all(bm, glow, swatch_uv("green")) set_uv_all(bm, walls, swatch_uv("white")) finish(bm) @@ -862,12 +943,14 @@ def main(): ap.add_argument("--D", type=float, default=0.760) ap.add_argument("--H", type=float, default=1.150) ap.add_argument("--render", action="store_true") - ap.add_argument("--no-pods", dest="pods", action="store_false", help="横の丸いふくらみ(輪の付いた横板)を付けない") + ap.add_argument("--pods", dest="pods", action="store_true", help="横に厚い板を貼る(2026-11-13 にやめた。横がパネルに見えたため)") + ap.add_argument("--crown", type=float, default=0.010, help="横の面のふくらみ(m)。0 で平ら") ap.add_argument("--belly", type=float, default=1.0, help="胴の角だけ、さらに丸める倍率") ap.add_argument("--soft", type=float, default=3.5, help="体の角の丸さの倍率(1 で三面図どおり。頭は常に三面図どおり)") args = ap.parse_args(argv) - global SOFT, BELLY, PODS + global SOFT, BELLY, PODS, CROWN PODS = args.pods + CROWN = args.crown SOFT = args.soft BELLY = args.belly os.makedirs(args.out, exist_ok=True)