マスコットロボット
7 面のイラストから起こした、全高 120 mm の卓上マスコット。
制作メモ
読み取り
7 方向のイラストが揃っていたので、正面図で縦横の比率を、上面図と側面図で 奥行きと前後位置を決められた。読み取った特徴は次のとおり。
- 頭は幅 > 奥行き > 高さ の角丸ボックス。丸みが大きくドームに近い
- 顔は黒い一枚のバイザー面で、頭の丸みに沿って側面まで回り込む
- バイザーの上に水色の目・笑い口、ふちにピンクのほお
- 側頭部に水色の円盤(耳)、頭頂に前後へ走る水色の帯、その前寄りにアンテナ
- 胸は水色の角丸パネル+黄色い 5 稜星
- 四肢は球の関節と丸棒の組み合わせ。手は 3 本指のクロー
- 靴は白い甲・水色のつま先・濃色の靴底の 3 色。底面図では靴底が一番目立つ
読み取れなかったのは内部構造と各パーツの分割線で、外形だけをモデル化した。
基準寸法
卓上に置くマスコットとして 全高 120 mm を仮定し、正面図の全高 400 px を 0.30 mm/px として他の寸法を換算した(頭幅 195 px → 58 mm など)。 出来上がりは 69 × 53.4 × 119.5 mm。縮尺を変えたいときは各定数を一律に 掛け直せばよく、比率はすべて先頭の定数に出してある。
モデリング方針
- 頭と胴は「角丸ボックス」1 つで表す。
_rounded_box(w, d, h, r, grow)に 太らせ量growを持たせ、同じ丸みの少し大きい/小さい形をいつでも作れるようにした - 顔・頭頂の帯・胸パネルなど表面の色パーツは、前向き(または上向き)に伸ばした柱を
外形(+proud) ∩ 柱 − 外形(+proud−厚み)で挟み、表面の曲率に沿った薄板として切り出す (_patch)。曲面に板を貼る式を 1 か所に閉じ込められるので、目・口・ほお・星まで同じ手が使える - 本体側は同じ薄板を
cutしてくぼみにしているので、パーツ同士が重ならない。 多色プリントや STEP での部品分けにそのまま使える - 四肢は関節座標(肩・肘・手首)を定数で置き、
_rodで 2 点間を丸棒でつなぐ。 姿勢を変えたいときは座標を動かすだけで済む - 左右対称なパーツは右半身だけ作り
_both_sidesで YZ 面に鏡像する - 色ごとに 5 つのパーツへまとめて
cq.Assemblyで返す(白・水色・黒・黄・ピンク)
割り切ったところ
- イラストの陰影から推測するしかない胴の微妙なふくらみは、角丸ボックス 1 つに寄せた
- 靴底のトレッド(溝)は入れていない。底面図にしか出ず、外形の印象を変えないため
- 手のクローは 3 本の指を平行に並べただけで、イラストのような内向きの湾曲は付けていない
- 首・関節は見た目の球のみで、可動機構は持たせていない
元画像
CADQuery ソース
"""7 面図のイラストから起こしたマスコットロボット。
構成は「角丸ボックスの頭と胴」+「関節球と丸棒でつないだ四肢」。
顔・胸・頭頂の色パーツは、頭/胴の外形を太らせた形と細らせた形で挟み込んで
(`_patch`)表面に沿った薄板として切り出している。こうすると丸みの式を
一度書くだけで済み、パーツが本体に埋まったり浮いたりしない。
単位はすべて mm。正面は −Y 方向、ロボットから見た右手側が +X。
"""
from __future__ import annotations
import math
from functools import lru_cache
import cadquery as cq
# 基準寸法: 卓上に置くマスコットとして全高 120 mm を仮定し、
# 正面図のピクセル比(全高 400 px)を 0.30 mm/px で mm に換算した。
# ---- 頭 ----------------------------------------------------------------
HEAD_W = 58.0 # 正面図 195 px
HEAD_D = 52.0 # 側面図から。幅よりわずかに浅い
HEAD_H = 48.0 # 正面図 170 px
HEAD_R = 18.0 # 角丸半径。大きく取ってドーム状にする
HEAD_Z = 80.0 # 頭の中心高さ
# ---- 顔(黒いバイザー面)------------------------------------------------
VISOR_W = 46.0
VISOR_H = 34.0
VISOR_R = 11.0 # バイザーの角丸
VISOR_DZ = -3.0 # 頭中心よりわずかに下げる
VISOR_T = 3.0 # 板厚。頭側には同じ深さのくぼみができる
VISOR_PROUD = 0.4 # 頭の外形からの出っ張り
FACE_T = 1.0 # 目・口・ほおの浮き出し厚
EYE_PITCH = 23.0 # 左右の目の間隔
EYE_W = 9.0
EYE_H = 11.0
EYE_DZ = 2.0 # バイザー中心からの上げ量
MOUTH_R = 7.0 # 笑い口の円弧半径
MOUTH_T = 1.6 # 線の太さ
MOUTH_DZ = -2.0 # 円弧の中心。線は下半分だけ使う
CHEEK_DIA = 8.5
CHEEK_X = 18.0
CHEEK_DZ = -6.0
# ---- 耳(側頭部の円盤)--------------------------------------------------
EAR_DIA = 22.0
EAR_PROUD = 4.0 # 頭の外形からの出っ張り
EAR_DY = 1.0
EAR_DZ = -3.0
# ---- 頭頂のライン ------------------------------------------------------
STRIPE_W = 18.0
STRIPE_L = 34.0
STRIPE_R = 8.0
STRIPE_DY = -3.0 # やや前寄り
STRIPE_T = 2.5
STRIPE_PROUD = 0.5
# ---- アンテナ ----------------------------------------------------------
ANT_ROD_DIA = 2.8
ANT_ROD_LEN = 10.0
ANT_BALL_DIA = 9.0
ANT_DY = -10.0 # 上面図に合わせて頭頂ラインの前寄りに立てる
# ---- 胴 ----------------------------------------------------------------
BODY_W = 46.0 # 正面図 155 px
BODY_D = 34.0
BODY_H = 38.0
BODY_R = 14.0
BODY_Z = 37.0 # 中心高さ。上端 56 が頭の下端に一致する
NECK_DIA = 22.0 # 頭と胴をつなぐ首。外からはほぼ見えない
# ---- 胸のパネルと星 ----------------------------------------------------
PANEL_W = 28.0
PANEL_H = 22.0
PANEL_R = 6.0
PANEL_DZ = 1.0
PANEL_T = 2.5
PANEL_PROUD = 0.5
STAR_R = 8.0 # 星の外接半径
STAR_WAIST = 0.48 # くぼみ側の半径比。小さいほど鋭い星になる
STAR_T = 1.2
# ---- 腕 ----------------------------------------------------------------
# 関節位置(右腕)。左腕は YZ 面で鏡像にする。
SHOULDER = (21.0, -1.0, 50.0)
ELBOW = (27.0, -3.0, 33.0)
WRIST = (29.0, -5.0, 22.0)
PALM = (30.0, -6.0, 18.0)
JOINT_DIA = 9.5 # 肩・肘の関節球
UPPER_ARM_DIA = 8.0
FOREARM_DIA = 7.5
PALM_DIA = 9.0
FINGER_DIA = 2.8
FINGER_LEN = 6.5
FINGER_SPREAD = 3.0 # 3 本の指を前後に振る間隔
FINGER_REACH = 2.0 # 指先を前へ出す量
# ---- 脚と靴 ------------------------------------------------------------
LEG_X = 12.0 # 左右の脚の中心
LEG_DIA = 16.0
HIP_Z = 23.0
ANKLE_Z = 10.5
FOOT_W = 18.0
FOOT_L = 24.0
FOOT_H = 13.0
FOOT_R = 5.5
FOOT_DY = -4.0 # つま先側へずらす
SOLE_H = 3.5 # 靴底(濃色)の厚み
TOE_L = 9.0 # つま先キャップ(水色)の長さ
# ---- 色 ----------------------------------------------------------------
WHITE = cq.Color(0.96, 0.97, 0.98)
CYAN = cq.Color(0.35, 0.76, 0.92)
BLACK = cq.Color(0.14, 0.16, 0.19)
YELLOW = cq.Color(0.98, 0.83, 0.28)
PINK = cq.Color(0.97, 0.68, 0.72)
# ---- 補助 --------------------------------------------------------------
def _rounded_box(w: float, d: float, h: float, r: float, grow: float) -> cq.Workplane:
"""原点中心の角丸ボックス。grow だけ全方向に太らせる(負なら細らせる)。"""
return (
cq.Workplane("XY")
.box(w + 2 * grow, d + 2 * grow, h + 2 * grow)
.edges()
.fillet(r + grow)
)
@lru_cache(maxsize=None)
def _head(grow: float = 0.0) -> cq.Workplane:
"""頭の外形。表面パーツの切り出しで繰り返し使うのでキャッシュする。"""
return _rounded_box(HEAD_W, HEAD_D, HEAD_H, HEAD_R, grow).translate((0, 0, HEAD_Z))
@lru_cache(maxsize=None)
def _torso(grow: float = 0.0) -> cq.Workplane:
"""胴の外形。"""
return _rounded_box(BODY_W, BODY_D, BODY_H, BODY_R, grow).translate((0, 0, BODY_Z))
def _patch(region: cq.Workplane, shell, proud: float, thickness: float) -> cq.Workplane:
"""柱状の region を、shell の表面に沿った薄板として切り出す。
外形を +proud 太らせた形で外側を止め、そこから thickness ぶん内側を抜く。
proud を正にすると表面から浮き、0 にすると面一(つらいち)になる。
"""
return region.intersect(shell(proud)).cut(shell(proud - thickness))
def _front_face() -> cq.Workplane:
"""正面向きの特徴を描くためのワークプレーン。
XZ 平面は面内が (X, Z)、押し出し方向が −Y なので、そのまま前向きの柱になる。
"""
return cq.Workplane("XZ")
def _rod(p1, p2, dia: float) -> cq.Workplane:
"""2 点を結ぶ丸棒。関節を座標で置けるので姿勢を作りやすい。"""
a, b = cq.Vector(*p1), cq.Vector(*p2)
axis = b - a
mid = (a + b) * 0.5
return (
cq.Workplane("XY")
.cylinder(axis.Length, dia / 2, direct=axis)
.translate(mid.toTuple())
)
def _ball(p, dia: float) -> cq.Workplane:
return cq.Workplane("XY").sphere(dia / 2).translate(p)
def _both_sides(part: cq.Workplane) -> cq.Workplane:
"""右半身のパーツを左右対称にする。"""
return part.union(part.mirror("YZ"))
# ---- 頭まわり ----------------------------------------------------------
def _visor() -> cq.Workplane:
"""顔の黒い面。頭の前半分だけを通る柱から切り出す(後頭部に回さない)。"""
region = (
_front_face()
.center(0, HEAD_Z + VISOR_DZ)
.rect(VISOR_W, VISOR_H)
.extrude(HEAD_D)
.edges("|Y")
.fillet(VISOR_R)
)
return _patch(region, _head, VISOR_PROUD, VISOR_T)
def _face_patch(region: cq.Workplane) -> cq.Workplane:
"""バイザーの上に乗る薄板(目・口・ほお)。"""
return _patch(region, _head, VISOR_PROUD + FACE_T, FACE_T)
def _eyes() -> cq.Workplane:
z = HEAD_Z + VISOR_DZ + EYE_DZ
return _face_patch(
_front_face()
.pushPoints([(-EYE_PITCH / 2, z), (EYE_PITCH / 2, z)])
.ellipse(EYE_W / 2, EYE_H / 2)
.extrude(HEAD_D)
)
def _mouth() -> cq.Workplane:
"""円環の下半分を笑い口にする。"""
z = HEAD_Z + VISOR_DZ + MOUTH_DZ
ring = (
_front_face()
.center(0, z)
.circle(MOUTH_R)
.circle(MOUTH_R - MOUTH_T)
.extrude(HEAD_D)
)
lower_half = (
_front_face()
.center(0, z - MOUTH_R / 2)
.rect(2 * MOUTH_R, MOUTH_R)
.extrude(HEAD_D)
)
return _face_patch(ring.intersect(lower_half))
def _cheeks() -> cq.Workplane:
z = HEAD_Z + VISOR_DZ + CHEEK_DZ
return _face_patch(
_front_face()
.pushPoints([(-CHEEK_X, z), (CHEEK_X, z)])
.circle(CHEEK_DIA / 2)
.extrude(HEAD_D)
)
def _ears() -> cq.Workplane:
"""側頭部の円盤。頭の外に出た分だけを残す。"""
ear = (
cq.Workplane("YZ", origin=(HEAD_W / 2 - EAR_DIA / 4, EAR_DY, HEAD_Z + EAR_DZ))
.circle(EAR_DIA / 2)
.extrude(EAR_DIA / 4 + EAR_PROUD)
.edges(">X")
.fillet(2.5)
.cut(_head(0.0))
)
return _both_sides(ear)
def _crown_stripe() -> cq.Workplane:
"""頭頂を前後に走る帯。"""
region = (
cq.Workplane("XY", origin=(0, STRIPE_DY, HEAD_Z))
.rect(STRIPE_W, STRIPE_L)
.extrude(HEAD_H)
.edges("|Z")
.fillet(STRIPE_R)
)
return _patch(region, _head, STRIPE_PROUD, STRIPE_T)
def _antenna() -> tuple[cq.Workplane, cq.Workplane]:
"""支柱と先端の球を返す。支柱は頭頂の帯の表面から生やす。"""
base_z = HEAD_Z + HEAD_H / 2 - 2.0
rod = (
cq.Workplane("XY", origin=(0, ANT_DY, base_z))
.circle(ANT_ROD_DIA / 2)
.extrude(ANT_ROD_LEN)
.cut(_head(STRIPE_PROUD))
)
ball = _ball((0, ANT_DY, base_z + ANT_ROD_LEN + ANT_BALL_DIA / 2 - 1.5), ANT_BALL_DIA)
return rod, ball
# ---- 胴まわり ----------------------------------------------------------
def _chest_panel() -> cq.Workplane:
region = (
_front_face()
.center(0, BODY_Z + PANEL_DZ)
.rect(PANEL_W, PANEL_H)
.extrude(BODY_D)
.edges("|Y")
.fillet(PANEL_R)
)
return _patch(region, _torso, PANEL_PROUD, PANEL_T)
def _star() -> cq.Workplane:
"""胸パネルの上に乗る 5 稜星。外周と谷の半径を交互に並べる。"""
points = []
for i in range(10):
angle = math.radians(90.0 + i * 36.0)
radius = STAR_R if i % 2 == 0 else STAR_R * STAR_WAIST
points.append((radius * math.cos(angle), radius * math.sin(angle)))
region = (
_front_face()
.center(0, BODY_Z + PANEL_DZ)
.polyline(points)
.close()
.extrude(BODY_D)
)
return _patch(region, _torso, PANEL_PROUD + STAR_T, STAR_T)
# ---- 四肢 --------------------------------------------------------------
def _arm() -> tuple[cq.Workplane, cq.Workplane, cq.Workplane]:
"""右腕を(白い腕, 水色の関節, 黒い手)で返す。"""
joints = _ball(SHOULDER, JOINT_DIA).union(_ball(ELBOW, JOINT_DIA))
hand = _ball(PALM, PALM_DIA)
for i in (-1, 0, 1):
tip = (PALM[0], PALM[1] + i * FINGER_SPREAD - FINGER_REACH, PALM[2] - FINGER_LEN)
hand = hand.union(_rod(PALM, tip, FINGER_DIA)).union(_ball(tip, FINGER_DIA))
arm = _rod(SHOULDER, ELBOW, UPPER_ARM_DIA).union(_rod(ELBOW, WRIST, FOREARM_DIA))
return arm.cut(joints).cut(hand), joints.cut(_torso(0.0)), hand
def _leg() -> cq.Workplane:
"""右脚。靴の中まで伸ばして胴とつなぐ。"""
return _rod((LEG_X, 0, ANKLE_Z), (LEG_X, 0, HIP_Z), LEG_DIA)
def _foot() -> tuple[cq.Workplane, cq.Workplane, cq.Workplane]:
"""右足を(白い甲, 水色のつま先, 濃色の靴底)で返す。"""
boot = (
cq.Workplane("XY", origin=(LEG_X, FOOT_DY, FOOT_H / 2))
.box(FOOT_W, FOOT_L, FOOT_H)
.edges()
.fillet(FOOT_R)
)
below_sole = cq.Workplane("XY", origin=(LEG_X, FOOT_DY, SOLE_H / 2)).box(
FOOT_W * 2, FOOT_L * 2, SOLE_H
)
toe_side = cq.Workplane(
"XY", origin=(LEG_X, FOOT_DY - FOOT_L / 2 + TOE_L / 2, FOOT_H)
).box(FOOT_W * 2, TOE_L, FOOT_H * 2)
upper = boot.cut(below_sole)
return upper.cut(toe_side), upper.intersect(toe_side), boot.intersect(below_sole)
# ---- 組み立て ----------------------------------------------------------
def build() -> cq.Assembly:
visor = _visor()
eyes, mouth, cheeks = _eyes(), _mouth(), _cheeks()
stripe, ears = _crown_stripe(), _ears()
ant_rod, ant_ball = _antenna()
panel, star = _chest_panel(), _star()
arm, joints, hand = _arm()
foot, toe, sole = _foot()
neck = cq.Workplane("XY", origin=(0, 0, BODY_Z + BODY_H / 2)).cylinder(12.0, NECK_DIA / 2)
# 白い本体。表面パーツが入る分(バイザー・帯・胸パネル)はくぼみとして抜く。
shell = (
_head(0.0)
.union(_torso(0.0))
.union(neck)
.union(_both_sides(arm))
.union(_both_sides(_leg()))
.union(_both_sides(foot))
.cut(visor)
.cut(stripe)
.cut(panel)
)
trim = (
stripe.union(ears)
.union(panel)
.union(ant_rod)
.union(eyes)
.union(mouth)
.union(_both_sides(joints))
.union(_both_sides(toe))
)
return (
cq.Assembly(name="robot-mascot")
.add(shell, name="shell", color=WHITE)
.add(trim, name="trim", color=CYAN)
.add(visor.union(_both_sides(hand)).union(_both_sides(sole)), name="face", color=BLACK)
.add(ant_ball.union(star), name="accents", color=YELLOW)
.add(cheeks, name="cheeks", color=PINK)
)
if __name__ == "__main__":
cq.exporters.export(build().toCompound(), "preview.stl")
